Compare commits
487 Commits
photos-v0.
...
auth-v4.2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a222d9dc4 | ||
|
|
cf4c20f2d3 | ||
|
|
28c29a0c3a | ||
|
|
3de1c8011a | ||
|
|
7adb166fad | ||
|
|
6447ba6ec0 | ||
|
|
258cdca69b | ||
|
|
2a19c30d0e | ||
|
|
d23c22762b | ||
|
|
bf9d0e3d6b | ||
|
|
a818f062b1 | ||
|
|
f57f1a8636 | ||
|
|
da78c45b1d | ||
|
|
3429a9f3d6 | ||
|
|
cdbd86d63c | ||
|
|
69e1aa18e6 | ||
|
|
6f2079c7c6 | ||
|
|
a14358416a | ||
|
|
57c353a443 | ||
|
|
dd7cea1f96 | ||
|
|
6d2a223acf | ||
|
|
eca0137426 | ||
|
|
e1e2c528c4 | ||
|
|
28ab3c321c | ||
|
|
5c2c6b2a84 | ||
|
|
4ae0683c62 | ||
|
|
0a6740bb2e | ||
|
|
b026020485 | ||
|
|
b71fa478b9 | ||
|
|
b9c992cae0 | ||
|
|
2845d7bfeb | ||
|
|
b09d6ab2a6 | ||
|
|
94ce77c07b | ||
|
|
a292f01187 | ||
|
|
aae2632b19 | ||
|
|
746c85bc9f | ||
|
|
adeab53d3b | ||
|
|
3e23ff9c9b | ||
|
|
dca6e02286 | ||
|
|
daf3fd2a75 | ||
|
|
f5a3b8a3fb | ||
|
|
7cd1ce0a99 | ||
|
|
1b863005ea | ||
|
|
b580756e6b | ||
|
|
26fb47c165 | ||
|
|
3f21011392 | ||
|
|
7348170a36 | ||
|
|
3919fb0db2 | ||
|
|
ad6a0e9c31 | ||
|
|
be3896826d | ||
|
|
268550f292 | ||
|
|
68e557124c | ||
|
|
96863923d1 | ||
|
|
a22b0aec58 | ||
|
|
93e26c6caf | ||
|
|
8a8f5c20c6 | ||
|
|
3c7b6694e9 | ||
|
|
c37d85f6c5 | ||
|
|
e0abb2de9c | ||
|
|
b73ba4a22f | ||
|
|
912279e3cf | ||
|
|
5a0bab9304 | ||
|
|
a9cd56c4ce | ||
|
|
5b4028378b | ||
|
|
57bd5b9d17 | ||
|
|
2bd074bd79 | ||
|
|
a8d831364d | ||
|
|
21f0602161 | ||
|
|
1ac2d60c7b | ||
|
|
a098481b98 | ||
|
|
472339cafb | ||
|
|
d24f5bcee7 | ||
|
|
bd0e8e6fe6 | ||
|
|
c9d2a0a4ca | ||
|
|
1a6eb26f2b | ||
|
|
7c2fce2ebe | ||
|
|
e90871ea6b | ||
|
|
3ca78cac35 | ||
|
|
49ddfdfde5 | ||
|
|
bc76864587 | ||
|
|
4f1d3c23f5 | ||
|
|
cc674183cd | ||
|
|
86bd098406 | ||
|
|
e5fe3a7255 | ||
|
|
df68d3f005 | ||
|
|
9a6a46fd0d | ||
|
|
14c9929451 | ||
|
|
54f9bd880a | ||
|
|
aaa636345c | ||
|
|
6f3e02888e | ||
|
|
42a8b5c826 | ||
|
|
24674f6da6 | ||
|
|
90b45665f5 | ||
|
|
8a217a292b | ||
|
|
047fede844 | ||
|
|
e77b557990 | ||
|
|
c2bfcf23c0 | ||
|
|
646a012734 | ||
|
|
21332c6b92 | ||
|
|
f9c101241e | ||
|
|
bc6f147f5e | ||
|
|
fbaa360741 | ||
|
|
4029398351 | ||
|
|
7602d48bd9 | ||
|
|
f28d50ace6 | ||
|
|
b026b30172 | ||
|
|
977f5c21a6 | ||
|
|
dafbf23d67 | ||
|
|
d9bc6597c0 | ||
|
|
913a80591b | ||
|
|
26c0a8e1d5 | ||
|
|
2e2c972a84 | ||
|
|
748dd2b0e2 | ||
|
|
dbb376056d | ||
|
|
b31fc5cbe9 | ||
|
|
cb76ba7560 | ||
|
|
e915ded2de | ||
|
|
eedc538283 | ||
|
|
294b333d0e | ||
|
|
8f705f2f72 | ||
|
|
c5c0ee5ddf | ||
|
|
5f43f03a65 | ||
|
|
aa62f4003c | ||
|
|
6c5dbc3696 | ||
|
|
1bef409552 | ||
|
|
62155040da | ||
|
|
592dc26d8b | ||
|
|
1af1c3f196 | ||
|
|
6f077310c1 | ||
|
|
905fc2ad78 | ||
|
|
e6e4f313de | ||
|
|
1ae8caa917 | ||
|
|
209228326d | ||
|
|
0533f99313 | ||
|
|
545cf40710 | ||
|
|
8ae8ed20fe | ||
|
|
0df0126af4 | ||
|
|
60ad6ef713 | ||
|
|
fc626c1287 | ||
|
|
4035e364df | ||
|
|
791ec10a0f | ||
|
|
87ab805cf7 | ||
|
|
447bb72556 | ||
|
|
c07e2f1387 | ||
|
|
151a0b3ab7 | ||
|
|
cf15d68bd2 | ||
|
|
e4a05edc12 | ||
|
|
879d6571bc | ||
|
|
2496350fad | ||
|
|
653a7f22ef | ||
|
|
6071df2083 | ||
|
|
f6e93ab060 | ||
|
|
ddc9dfe552 | ||
|
|
b9b87c1570 | ||
|
|
3a413524f8 | ||
|
|
4830451d4e | ||
|
|
4cb11f4b06 | ||
|
|
150faa5d84 | ||
|
|
9b15102058 | ||
|
|
25bb175ff7 | ||
|
|
ec7b61c36a | ||
|
|
85735b4ff0 | ||
|
|
8029829d9b | ||
|
|
87c5f05f84 | ||
|
|
356b2542c9 | ||
|
|
dc3329368e | ||
|
|
92868dccb4 | ||
|
|
49942909b0 | ||
|
|
b36c8de417 | ||
|
|
a0335b82c6 | ||
|
|
01cbf29217 | ||
|
|
7c464a0d60 | ||
|
|
8c20f5f660 | ||
|
|
671199e286 | ||
|
|
6805ee1a2a | ||
|
|
fd8246705c | ||
|
|
65e1745aa0 | ||
|
|
c6c3b1f9bd | ||
|
|
175467267a | ||
|
|
9756c178bf | ||
|
|
af420a8fc3 | ||
|
|
9a5d977419 | ||
|
|
d4ae5c118b | ||
|
|
605fda2710 | ||
|
|
0181693736 | ||
|
|
5c92d093ca | ||
|
|
a6c9a153e7 | ||
|
|
613f7294e1 | ||
|
|
facd05bd89 | ||
|
|
4bbe71e135 | ||
|
|
5583902433 | ||
|
|
664c723c78 | ||
|
|
9b35fe04b9 | ||
|
|
34068d09ba | ||
|
|
c23b22cc5b | ||
|
|
065382ddd2 | ||
|
|
5a72686e53 | ||
|
|
8c5b77cd52 | ||
|
|
d30dce0896 | ||
|
|
c453827cc8 | ||
|
|
1068b6811f | ||
|
|
09fe2c6f7e | ||
|
|
a31803e3f5 | ||
|
|
2ec8ae34b8 | ||
|
|
3263542f5e | ||
|
|
a302f986d7 | ||
|
|
69ccf7d3c9 | ||
|
|
76308cc9d0 | ||
|
|
1d02732719 | ||
|
|
55fa86a6c8 | ||
|
|
39fad29bc8 | ||
|
|
2ebe8712e8 | ||
|
|
1f7176cea2 | ||
|
|
bede7559be | ||
|
|
77563a7483 | ||
|
|
37df79314a | ||
|
|
fc5d1f931c | ||
|
|
243948f182 | ||
|
|
ddc953045b | ||
|
|
f74f285b7f | ||
|
|
2a4a886fca | ||
|
|
43e3e44e5c | ||
|
|
09cc226511 | ||
|
|
d9f62b8956 | ||
|
|
a691745ef7 | ||
|
|
108e984f29 | ||
|
|
fbbb8edce1 | ||
|
|
72dd4949ce | ||
|
|
948b869bea | ||
|
|
3cde395f42 | ||
|
|
05165728f1 | ||
|
|
42f2bb819b | ||
|
|
6b28aa1652 | ||
|
|
d167da02d5 | ||
|
|
34fe3bee7f | ||
|
|
4bfa398312 | ||
|
|
5920999bf4 | ||
|
|
7559ab4236 | ||
|
|
44d2f66260 | ||
|
|
ae3b4604e9 | ||
|
|
7b32ace2d9 | ||
|
|
29052e2888 | ||
|
|
b18d8bb5e6 | ||
|
|
91aea808f5 | ||
|
|
a6590c29d5 | ||
|
|
b139dea7ff | ||
|
|
80857d5441 | ||
|
|
1fb9a7e6c4 | ||
|
|
2bc1c90637 | ||
|
|
23a33610ee | ||
|
|
a6cd937347 | ||
|
|
7f3d0a5328 | ||
|
|
40d938b6a3 | ||
|
|
423a669cff | ||
|
|
4546d60e61 | ||
|
|
735f5e3d3c | ||
|
|
7cf7443177 | ||
|
|
d66aa25ee7 | ||
|
|
0d920a35e7 | ||
|
|
38860d91d3 | ||
|
|
941b326328 | ||
|
|
6851d0fae5 | ||
|
|
42dc8a451b | ||
|
|
8e2f052ac3 | ||
|
|
087f34304b | ||
|
|
427c5b4d7d | ||
|
|
bf3a47826b | ||
|
|
423ebc6588 | ||
|
|
9a89153563 | ||
|
|
606e3013f7 | ||
|
|
653ae485a9 | ||
|
|
f681c956ea | ||
|
|
4422b4a7b0 | ||
|
|
926b5de6cd | ||
|
|
41de48c454 | ||
|
|
2f3f48f4db | ||
|
|
71ea266f9a | ||
|
|
af187a3c0c | ||
|
|
11d32752d4 | ||
|
|
8a3b0d956e | ||
|
|
183321fa18 | ||
|
|
96d9c6d5ee | ||
|
|
728cd31210 | ||
|
|
9491310bfd | ||
|
|
1978226fce | ||
|
|
956e74533e | ||
|
|
1b5e6174f1 | ||
|
|
8fc9ff0d9f | ||
|
|
8e6330dfdb | ||
|
|
eec4bbde98 | ||
|
|
b12371437f | ||
|
|
8219177c1c | ||
|
|
56be41c38f | ||
|
|
5b371380fd | ||
|
|
eec060ae71 | ||
|
|
70ec18462b | ||
|
|
b3cf07f232 | ||
|
|
2f3639fbbc | ||
|
|
c1ac9d22ba | ||
|
|
3fdfa10402 | ||
|
|
6b49a889da | ||
|
|
431ad61ca2 | ||
|
|
75456c1b34 | ||
|
|
45b490cb43 | ||
|
|
1f4aebf20f | ||
|
|
e639aa9306 | ||
|
|
5ce96fde3e | ||
|
|
a91027c335 | ||
|
|
1ad7ba82c2 | ||
|
|
b5d274f7ae | ||
|
|
35f710439f | ||
|
|
bb2072aafe | ||
|
|
479f172e4d | ||
|
|
4383841ef1 | ||
|
|
57a00c1703 | ||
|
|
9b292bbd80 | ||
|
|
8ffb52dd7e | ||
|
|
a9545c3bef | ||
|
|
b0f1cea9ef | ||
|
|
2c9cff040d | ||
|
|
9aaee77004 | ||
|
|
9ac61d063a | ||
|
|
3e205ac275 | ||
|
|
277d7fa0cd | ||
|
|
51734d96d5 | ||
|
|
c4880fd07e | ||
|
|
75ae277334 | ||
|
|
726cfc8bf2 | ||
|
|
6b6db069b0 | ||
|
|
1459678d70 | ||
|
|
ce3e8bf315 | ||
|
|
adaf70695b | ||
|
|
f1e17948c4 | ||
|
|
abeac7aa49 | ||
|
|
d46b7a8189 | ||
|
|
d312761166 | ||
|
|
c29d857b83 | ||
|
|
7e79c9d847 | ||
|
|
55d0a1a0b5 | ||
|
|
6e14a3af09 | ||
|
|
7ce5306f53 | ||
|
|
34e6e71b34 | ||
|
|
3fd4717ec4 | ||
|
|
20acf7c0bf | ||
|
|
46e764d3db | ||
|
|
c6faaf8aa9 | ||
|
|
f5e77d8b23 | ||
|
|
449c966342 | ||
|
|
ca02c20d09 | ||
|
|
8870a8ec4a | ||
|
|
4150e607eb | ||
|
|
0a5d31da18 | ||
|
|
e3d3f3b1f4 | ||
|
|
062a7fe257 | ||
|
|
442cf6583b | ||
|
|
7bc38a061d | ||
|
|
fd04cd0a98 | ||
|
|
1a80c40f78 | ||
|
|
19441d9fee | ||
|
|
826b2f997e | ||
|
|
747bf88515 | ||
|
|
5175f24402 | ||
|
|
32ed84f48d | ||
|
|
ca40492d99 | ||
|
|
c220e0385a | ||
|
|
df9c08cd7f | ||
|
|
a7cd7030f1 | ||
|
|
113fbef0d9 | ||
|
|
af23fd37b2 | ||
|
|
6dc2f6139a | ||
|
|
665ba9e634 | ||
|
|
acd2f63a87 | ||
|
|
58ae5eee32 | ||
|
|
eaee515e17 | ||
|
|
3be7c4a60f | ||
|
|
84e39c43d6 | ||
|
|
7f0d07db3f | ||
|
|
0fd6eceda6 | ||
|
|
f439d805fc | ||
|
|
0e98ef43df | ||
|
|
38d679f574 | ||
|
|
be92c30bb1 | ||
|
|
c648127ff8 | ||
|
|
209bdf3f0b | ||
|
|
257344f2e5 | ||
|
|
cbe105020b | ||
|
|
0e33013cec | ||
|
|
9c0426d716 | ||
|
|
35de887624 | ||
|
|
d81c545423 | ||
|
|
c499df4212 | ||
|
|
e0d462ec75 | ||
|
|
e81986a27c | ||
|
|
27c66ce2f7 | ||
|
|
c80f64943d | ||
|
|
5326c7452b | ||
|
|
38023d0ab1 | ||
|
|
401ed5bf9c | ||
|
|
6cc6d2521a | ||
|
|
553276828c | ||
|
|
e5743fceed | ||
|
|
b6d9527f1d | ||
|
|
64594c5077 | ||
|
|
56e3f23a56 | ||
|
|
12927b6f82 | ||
|
|
92208b7d21 | ||
|
|
e4c35b404e | ||
|
|
e9a8449a64 | ||
|
|
575d220b31 | ||
|
|
4d280fd14b | ||
|
|
22e4f6bc94 | ||
|
|
ca95ea8de2 | ||
|
|
d1625361d7 | ||
|
|
7aedfb7e9b | ||
|
|
00b722a0a5 | ||
|
|
9da5f6c99d | ||
|
|
fbf2a2bb23 | ||
|
|
e6a2cb0e57 | ||
|
|
61e1ea4f42 | ||
|
|
1547b04ddf | ||
|
|
9658cde381 | ||
|
|
dec2ee7202 | ||
|
|
0cb79102fd | ||
|
|
2e2e381100 | ||
|
|
815d9e8972 | ||
|
|
5893c927c2 | ||
|
|
da89c02505 | ||
|
|
3d58a8cf5a | ||
|
|
33c497e101 | ||
|
|
0ec493836c | ||
|
|
e5ccf494c5 | ||
|
|
8692421b9a | ||
|
|
7b85d216dd | ||
|
|
ebf92dba94 | ||
|
|
c4799a719b | ||
|
|
1222a063e8 | ||
|
|
c5c77ab706 | ||
|
|
7952257a89 | ||
|
|
8db40c5c58 | ||
|
|
051b197180 | ||
|
|
a46dd1f447 | ||
|
|
394d98ca46 | ||
|
|
1cf5875a6f | ||
|
|
77856f2b6d | ||
|
|
7b689f4197 | ||
|
|
25a3ce2909 | ||
|
|
5d474350c2 | ||
|
|
5aa2527021 | ||
|
|
62469dec0b | ||
|
|
ff41f1c7f8 | ||
|
|
2fc2107bca | ||
|
|
0e80508f62 | ||
|
|
ee7c7a447d | ||
|
|
16e8aa3803 | ||
|
|
e6fa7d4e21 | ||
|
|
0e677f052b | ||
|
|
5c4c489912 | ||
|
|
58a00a8758 | ||
|
|
9650eb3ff6 | ||
|
|
32f8075acf | ||
|
|
9fb1dbf67e | ||
|
|
f68f0a5ea8 | ||
|
|
c5996ffc9c | ||
|
|
06cad1b996 | ||
|
|
2a4b15ea48 | ||
|
|
c0bbad8f88 | ||
|
|
b5c2991575 | ||
|
|
f439f2fcec | ||
|
|
8b01129cc9 | ||
|
|
58486744e1 | ||
|
|
a0d46ac60e | ||
|
|
2430473a10 | ||
|
|
4f963f250f | ||
|
|
ca70c36ae0 | ||
|
|
1bf8f2749e | ||
|
|
56b8728e79 | ||
|
|
df28a8bf50 | ||
|
|
0a446a6629 | ||
|
|
b8f1bce341 | ||
|
|
be615197fd | ||
|
|
d4a68069ba | ||
|
|
8fc14c72e2 | ||
|
|
69d75644d0 | ||
|
|
1fabaf9aaa | ||
|
|
aa482ea227 | ||
|
|
41c242a0ee | ||
|
|
c2e53c6ec9 |
12
.github/workflows/auth-lint.yml
vendored
@@ -30,6 +30,18 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify all icons are less than 20KB
|
||||
run: |
|
||||
find assets/custom-icons -type f -name "*.svg" | while read -r file; do
|
||||
if [[ "$file" == "assets/custom-icons/icons/bbs_nga.svg" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$(stat --printf="%s" "$file")" -gt 20480 ]]; then
|
||||
echo "File size is greater than 20KB: $file ($file_size bytes)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Verify custom icon JSON
|
||||
run: cat assets/custom-icons/_data/custom-icons.json | jq empty
|
||||
|
||||
2
.github/workflows/auth-release.yml
vendored
@@ -151,7 +151,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 libtiff5 xz-utils libarchive-tools
|
||||
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
|
||||
|
||||
@@ -30,10 +30,10 @@ Learn more at [ente.io](https://ente.io).
|
||||
|
||||

|
||||
|
||||
Our flagship product. 3x data replication. On device machine learning. Cross
|
||||
platform. Private sharing. Collaborative albums. Family plans. Easy import,
|
||||
easier export. Background uploads. The list goes on. And of course, all of this,
|
||||
while being fully end-to-end encrypted.
|
||||
Our flagship product. 3x data replication. Face detection. Semantic search.
|
||||
Private sharing. Collaborative albums. Family plans. Easy import, easier export.
|
||||
Background uploads. The list goes on. And of course, all of this, while being
|
||||
fully end-to-end encrypted across platforms.
|
||||
|
||||
Ente Photos is a paid service, but we offer 5GB of free storage.
|
||||
You can also clone this repository and choose to self-host.
|
||||
|
||||
@@ -269,6 +269,10 @@
|
||||
{
|
||||
"title": "Dropbox"
|
||||
},
|
||||
{
|
||||
"title": "DreamHost Panel",
|
||||
"slug": "dreamhost_panel"
|
||||
},
|
||||
{
|
||||
"title": "dus.net",
|
||||
"slug": "dusnet"
|
||||
@@ -444,6 +448,9 @@
|
||||
{
|
||||
"title": "Kite"
|
||||
},
|
||||
{
|
||||
"title": "Kotas"
|
||||
},
|
||||
{
|
||||
"title": "KnownHost",
|
||||
"altNames": [
|
||||
@@ -526,7 +533,9 @@
|
||||
},
|
||||
{
|
||||
"title": "matlab",
|
||||
"altNames": ["mathworks"]
|
||||
"altNames": [
|
||||
"mathworks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mercado Livre",
|
||||
@@ -613,6 +622,9 @@
|
||||
"title": "ngrok",
|
||||
"hex": "858585"
|
||||
},
|
||||
{
|
||||
"title": "Nelnet"
|
||||
},
|
||||
{
|
||||
"title": "nintendo",
|
||||
"altNames": [
|
||||
@@ -625,8 +637,10 @@
|
||||
{
|
||||
"title": "nordvpn",
|
||||
"slug": "nordaccount",
|
||||
"hex": "#4687FF",
|
||||
"altNames": "Nord Account"
|
||||
"hex": "4687FF",
|
||||
"altNames": [
|
||||
"Nord Account"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Notesnook"
|
||||
@@ -732,7 +746,8 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "randstad"
|
||||
"title": "randstad",
|
||||
"hex": "2175D9"
|
||||
},
|
||||
{
|
||||
"title": "Real-Debrid",
|
||||
@@ -964,6 +979,10 @@
|
||||
{
|
||||
"title": "Upstox"
|
||||
},
|
||||
{
|
||||
"title": "US Mobile",
|
||||
"slug": "us_mobile"
|
||||
},
|
||||
{
|
||||
"title": "Vikunja"
|
||||
},
|
||||
@@ -1027,4 +1046,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
14
auth/assets/custom-icons/icons/dreamhost_panel.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="160.5px" height="160.5px" viewBox="0 0 160.5 160.5" enable-background="new 0 0 160.5 160.5" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M145.794,113.854c-11.111,5.932-23.811,9.227-37.417,9.008c-40.366-0.879-72.114-32.293-71.434-70.515
|
||||
c0.227-12.961,4.309-25.042,11.112-35.148C26.512,28.841,11.771,50.589,11.318,75.851c-0.907,38.002,31.068,69.636,71.208,70.515
|
||||
C109.284,146.805,132.642,133.844,145.794,113.854z"/>
|
||||
<path fill="#0073EC" d="M86.833,14.123c-10.885,0-21.09,2.636-30.161,7.469c-5.669,9.007-8.844,19.331-9.297,29.875
|
||||
c-0.68,33.171,27.212,60.63,61.909,61.289c10.432,0,22.224-2.417,31.294-7.249c5.443-9.007,8.617-19.551,8.617-30.754
|
||||
C149.648,41.362,121.53,14.123,86.833,14.123z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 19 KiB |
5
auth/assets/custom-icons/icons/kotas.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<g style="mix-blend-mode:difference">
|
||||
<path d="M105.08 0.61C90.5 2.16 71.47 8.21 57.3 15.82C28.49 31.36 10.26 55.31 3.69 86.29C0.94 99.26 1.00 94.06 1.00 249.17C1.00 386.74 1.06 395.6 2.03 401.78C5.40 423.61 13.35 442.47 25.46 457.5C30.32 463.56 40.49 473.27 46.27 477.39C65.24 490.93 88.90 498.99 112.16 499.91C139.48 500.99 165.60 491.62 188.29 472.53C194.29 467.50 222.01 440.24 233.90 427.72C241.45 419.78 244.13 418.06 249.33 418.12C255.22 418.12 258.88 420.86 269.62 433.15C278.20 442.93 306.54 470.07 314.09 475.67C324.49 483.45 332.66 488.02 343.92 492.36C388.79 509.62 438.68 497.05 470.23 460.47C504.47 420.86 507.90 364.91 478.80 320.90C472.18 310.79 466.52 304.56 441.48 279.58C424.57 262.72 417.59 255.35 416.79 253.58C414.05 247.57 414.96 246.20 435.43 226.14C456.63 205.34 473.20 187.91 477.55 181.85C488.18 166.99 494.35 152.47 497.21 135.67C498.64 127.04 498.64 108.52 497.15 99.72C491.38 64.23 475.20 38.05 448.00 19.93C428.68 7.07 407.19 0.61 383.82 0.56C356.27 0.56 332.66 8.84 311.34 26.10C305.57 30.79 276.77 58.97 263.56 72.91C256.36 80.51 254.02 82.17 249.85 82.69C248.19 82.91 246.88 82.63 244.48 81.49C241.96 80.23 239.39 77.83 231.10 68.97C212.18 48.68 193.15 30.56 182.75 22.85C158.74 5.19 132.74 -2.30 105.08 0.61ZM397.19 49.37C405.71 51.19 413.65 54.51 420.79 59.14C426.85 63.03 437.43 73.88 441.26 80.17C453.43 99.83 454.91 122.69 445.31 142.87C440.17 153.67 439.20 154.81 397.59 196.88C356.95 237.97 315.80 279.01 289.86 304.33C277.74 316.22 274.37 319.82 273.17 322.27C271.28 326.16 271.17 329.36 272.94 332.50C274.71 335.76 292.66 353.94 295.51 355.42C298.66 357.02 302.49 356.97 305.80 355.31C307.46 354.51 310.43 351.82 314.15 347.76C317.29 344.28 331.46 329.88 345.64 315.70C363.07 298.27 372.10 289.58 373.64 288.90C376.44 287.64 380.04 287.64 382.44 288.90C385.19 290.33 433.02 338.73 437.37 344.51C448.28 359.02 453.31 377.08 451.03 393.54C446.23 428.35 411.42 457.04 378.67 453.33C365.24 451.78 354.38 447.16 342.55 437.90C336.66 433.32 161.55 258.78 159.03 255.00C156.74 251.58 156.74 248.60 158.97 245.12C159.83 243.75 175.20 228.09 193.09 210.31C213.10 190.48 226.01 177.22 226.64 175.90C228.81 171.33 227.33 167.56 221.21 162.19C218.98 160.19 214.58 156.24 211.50 153.44C198.64 141.67 197.72 141.15 194.01 143.15C191.61 144.41 104.79 231.80 101.42 236.32C95.65 244.09 94.56 250.49 97.82 257.52C98.79 259.63 101.08 263.18 102.91 265.41C104.68 267.64 129.25 292.55 157.49 320.79C188.58 351.94 209.21 373.03 209.90 374.34C211.38 377.26 211.38 381.20 209.84 384.23C209.15 385.60 206.01 389.09 202.52 392.29C199.21 395.43 187.61 406.92 176.75 417.84C155.49 439.27 151.20 442.87 141.60 447.56C127.14 454.59 111.88 455.44 95.88 450.13C73.59 442.64 56.78 427.09 50.95 408.63C47.69 398.29 47.92 409.66 47.75 253.46C47.58 126.12 47.69 109.32 48.44 103.78C50.61 88.86 56.61 77.08 67.07 67.14C76.56 58.11 88.96 51.65 101.13 49.37C108.68 47.99 122.51 48.45 130.00 50.45C141.08 53.37 150.23 57.94 158.92 64.91C161.37 66.85 198.18 103.43 240.87 146.18C301.86 207.39 319.00 224.26 321.29 225.40C325.35 227.46 329.23 227.40 333.06 225.23C336.09 223.51 341.24 218.37 347.98 210.31C355.12 201.79 356.21 198.19 353.07 192.82C351.07 189.39 322.09 159.67 297.11 135.44C285.74 124.41 284.54 121.27 289.11 114.52C290.20 112.86 292.71 110.18 294.66 108.52C296.60 106.86 307.57 96.06 319.06 84.57C337.58 65.94 340.61 63.14 346.09 59.54C354.67 53.94 364.50 50.11 374.21 48.57C379.01 47.82 392.27 48.28 397.19 49.37Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
33
auth/assets/custom-icons/icons/nelnet.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="167.5px" height="167.5px" viewBox="0 0 167.5 167.5" enable-background="new 0 0 167.5 167.5" xml:space="preserve">
|
||||
<g>
|
||||
<defs>
|
||||
<rect id="SVGID_1_" x="3.75" y="3.75" width="160" height="160"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_2_">
|
||||
<use xlink:href="#SVGID_1_" overflow="visible"/>
|
||||
</clipPath>
|
||||
<g id="icon-circle-n" clip-path="url(#SVGID_2_)">
|
||||
<rect x="3.75" y="3.75" fill="#FFFFFF" width="160" height="160"/>
|
||||
<g id="Group_1" transform="translate(-283.001 134)">
|
||||
<path id="Path_926" fill="#A2AAAD" d="M371.633-89.707c-8.596,0-14.667,3.859-19.81,8.422l-0.283-5.307
|
||||
c-0.027-0.655-0.562-1.173-1.217-1.178h-16.4c-0.68,0.004-1.229,0.56-1.224,1.24c0,0.001,0,0.003,0,0.004v74.489
|
||||
c-0.003,0.678,0.542,1.23,1.221,1.236h18.258c0.68-0.004,1.228-0.557,1.225-1.236v-51.36c6.662-7.678,9.619-8.926,14.025-8.926
|
||||
c6.132,0,12.679,4.229,12.679,16.1v44.186c-0.004,0.678,0.543,1.231,1.221,1.236c0,0,0,0,0.001,0h18.259
|
||||
c0.678-0.006,1.225-0.559,1.221-1.236v-47.985C400.81-79.507,386.134-89.707,371.633-89.707"/>
|
||||
|
||||
<linearGradient id="Path_929_1_" gradientUnits="userSpaceOnUse" x1="59.4341" y1="340.5049" x2="59.4341" y2="339.5049" gradientTransform="matrix(160 0 0 -160 -9142.6602 54350.5)">
|
||||
<stop offset="0" style="stop-color:#AFD135"/>
|
||||
<stop offset="1" style="stop-color:#70BA44"/>
|
||||
</linearGradient>
|
||||
<path id="Path_929" fill="url(#Path_929_1_)" d="M366.751-130.25c-44.162,0.05-79.95,35.838-80,80l0,0
|
||||
c0.051,44.162,35.838,79.949,80,80l0,0c44.162-0.051,79.949-35.838,80-80l0,0C446.701-94.412,410.913-130.2,366.751-130.25z
|
||||
M303.223-50.25c0.04-35.069,28.459-63.487,63.528-63.527l0,0c35.065,0.043,63.479,28.459,63.521,63.525l0,0
|
||||
c-0.042,35.065-28.456,63.481-63.521,63.525l0,0c-35.066-0.043-63.483-28.459-63.524-63.525"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,19 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0" width="990.35431" height="207.28346" id="svg2">
|
||||
<defs id="defs4"/>
|
||||
<g transform="translate(116.92129,-338.61963)" id="layer1">
|
||||
<g transform="translate(35.40372,35.392161)" id="g31225">
|
||||
<path d="M -28.795615,415.09001 C -28.795615,415.09001 -59.115451,384.77025 -59.125465,384.77025 C -60.927824,382.91774 -63.370971,381.90646 -66.024482,381.90646 L -116.92129,381.90646 L -116.92129,401.27187 L -61.19811,401.27187 C -56.952604,401.27187 -52.957357,402.92402 -49.953371,405.92801 C -46.949465,408.93191 -45.297309,412.91715 -45.297309,417.17275 L -45.297309,472.88583 L -25.941913,472.88583 L -25.941913,421.98902 C -25.941913,419.34561 -26.953202,416.89237 -28.795615,415.09001" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path38"/>
|
||||
<path d="M 25.195234,381.90646 C 22.551768,381.90646 20.09856,382.91774 18.296201,384.77025 L -12.023635,415.09001 C -13.866049,416.89237 -14.877338,419.34561 -14.877338,421.98902 L -14.877338,472.88583 L 4.4780586,472.88583 L 4.4780586,417.17275 C 4.4780586,408.40119 11.607388,401.27187 20.378939,401.27187 L 76.09204,401.27187 L 76.09204,381.90646 L 25.195234,381.90646" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path42"/>
|
||||
<path d="M 209.20675,379.69354 C 199.83445,379.69354 190.17176,386.31226 185.7059,395.80471 L 185.5557,396.11513 L 184.29404,396.11513 L 184.29404,381.90646 L 169.60477,381.90646 L 169.60477,472.88583 L 185.36545,472.88583 L 185.36545,431.53162 C 185.36545,408.36114 195.43867,393.39152 211.00911,393.39152 C 214.37352,393.39152 217.30737,393.91222 219.85071,394.8434 L 219.85071,380.85511 C 216.47628,380.17419 213.99302,379.69354 209.20675,379.69354" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path46"/>
|
||||
<path d="M 694.79406,388.47511 C 688.87629,382.54732 679.95458,379.67351 667.50825,379.67351 C 657.2648,379.67351 647.27168,382.50727 637.79925,388.04452 L 638.61032,401.2218 C 645.55944,395.64449 655.47244,392.18997 664.8047,392.18997 C 672.78517,392.18997 678.48265,394.01236 682.23757,397.76728 C 686.21279,401.73249 688.06522,407.81047 688.06522,416.88235 L 688.06522,417.47315 L 687.48446,417.4331 C 680.77566,417.07261 676.03944,417.07261 669.49085,417.08262 C 658.30617,417.07261 643.26643,420.16664 634.56501,428.86809 C 629.74868,433.68437 627.30548,439.74233 627.30548,446.85171 C 627.30548,455.20262 629.70863,461.83135 634.44485,466.57761 C 640.04219,472.16493 649.04401,475.11877 660.47902,475.11877 C 676.93061,475.11877 685.55192,465.50616 688.49579,461.37073 L 688.656,461.14041 L 689.85757,461.14041 L 689.85757,472.8658 L 703.82591,472.8658 L 703.82591,417.17275 C 703.82591,403.93539 700.88205,394.55301 694.79406,388.47511 z M 688.06522,428.93819 L 688.06522,437.61954 C 688.06522,444.83907 685.75219,450.99717 681.39648,455.43293 C 676.7704,460.12912 670.04157,462.60239 661.92091,462.61241 C 656.97442,462.61241 651.83768,460.53968 648.5033,457.21526 C 645.72966,454.43165 644.24772,450.88702 644.24772,446.95185 C 644.24772,430.37005 666.9375,428.5777 676.70031,428.5777 C 678.51268,428.5777 680.35511,428.66782 682.12743,428.75795 C 683.89976,428.84807 685.72215,428.93819 687.5145,428.93819 L 688.06522,428.93819" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path50"/>
|
||||
<path d="M 285.2165,388.47511 C 279.28873,382.54732 270.36701,379.67351 257.92068,379.67351 C 247.67724,379.67351 237.68412,382.50727 228.21168,388.04452 L 229.02275,401.2218 C 235.97188,395.64449 245.88488,392.18997 255.21713,392.18997 C 263.1976,392.18997 268.9051,394.01236 272.65001,397.76728 C 276.62523,401.73249 278.47766,407.81047 278.47766,416.88235 L 278.47766,417.47315 L 277.8969,417.4331 C 271.1981,417.07261 266.46189,417.07261 259.90328,417.08262 C 248.72861,417.07261 233.67886,420.16664 224.98746,428.86809 C 220.16112,433.68437 217.71791,439.74233 217.71791,446.85171 C 217.71791,455.20262 220.12107,461.83135 224.86729,466.57761 C 230.45463,472.16493 239.45645,475.11877 250.89145,475.11877 C 267.34305,475.11877 275.96436,465.50616 278.90823,461.37073 L 279.07845,461.14041 L 280.27001,461.14041 L 280.27001,472.8658 L 294.24836,472.8658 L 294.24836,417.17275 C 294.24836,403.93539 291.28447,394.55301 285.2165,388.47511 z M 278.47766,428.93819 L 278.47766,437.61954 C 278.47766,444.83907 276.16463,450.99717 271.80891,455.43293 C 267.19285,460.12912 260.454,462.60239 252.33335,462.61241 C 247.38685,462.61241 242.25011,460.53968 238.92575,457.21526 C 236.1421,454.43165 234.67016,450.88702 234.67016,446.95185 C 234.67016,430.37005 257.35994,428.5777 267.11274,428.5777 C 268.92512,428.5777 270.76754,428.66782 272.54988,428.75795 C 274.31219,428.84807 276.13459,428.93819 277.92694,428.93819 L 278.47766,428.93819" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path54"/>
|
||||
<path d="M 379.59037,388.13464 C 373.973,382.51728 365.87236,379.67351 355.50876,379.67351 C 342.50169,379.67351 330.55601,386.25218 325.79976,396.05504 L 325.64957,396.37548 L 324.38792,396.37548 L 324.38792,381.92648 L 309.16794,381.92648 L 309.16794,472.88583 L 324.93864,472.88583 L 324.93864,430.84068 C 324.93864,419.03519 327.24166,409.67292 331.78763,403.02416 C 336.70408,395.83475 344.02369,392.18997 353.54618,392.18997 C 367.97512,393.0811 373.6025,402.4634 373.6025,425.67393 L 373.6025,472.88583 L 389.37321,472.88583 L 389.37321,418.61462 C 389.37321,404.87658 386.07888,394.62311 379.59037,388.13464" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path58"/>
|
||||
<path d="M 533.222,420.44702 C 524.52058,416.4718 516.29979,412.72689 516.29979,403.99547 C 516.29979,396.81599 522.87843,392.18997 533.06179,392.18997 C 536.77666,392.18997 546.07887,394.27271 550.45461,396.5957 L 551.76634,383.16808 C 544.79719,381.06532 538.58904,379.67351 531.25943,379.67351 C 520.74563,379.67351 512.20441,382.65747 506.56701,388.29486 C 501.78074,393.0811 499.35755,399.51957 499.35755,407.43997 C 499.35755,421.45839 511.46344,427.37616 522.14746,432.60299 C 530.91898,436.87862 539.18983,440.93395 539.18983,448.81429 C 539.18983,452.37896 538.01829,455.36283 535.70526,457.67588 C 531.25943,462.12174 524.18013,462.60239 521.35643,462.60239 C 516.85052,462.60239 507.67848,460.66986 500.73937,456.45431 L 499.91829,470.32252 C 508.10904,474.55809 517.39122,475.11877 524.59067,475.11877 C 533.09183,475.11877 540.86203,472.70559 546.44936,468.29986 C 552.78767,463.30326 556.14208,455.94362 556.14208,446.9919 C 556.14208,430.90077 543.9661,425.3535 533.222,420.44702" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path62"/>
|
||||
<path d="M 611.11423,462.61241 C 607.20911,462.61241 603.6244,461.16044 601.01097,458.55708 C 597.92692,455.47298 596.3048,450.97714 596.3048,445.55997 L 596.3048,394.42292 L 620.66677,394.42292 L 620.66677,381.90646 L 596.3048,381.90646 L 596.3048,355.4017 L 580.52408,360.46832 L 580.52408,381.90646 L 559.76684,381.90646 L 559.76684,394.42292 L 580.52408,394.42292 L 580.52408,449.00455 C 580.52408,457.29537 582.747,463.71382 587.13276,468.08958 C 591.78887,472.75565 598.88818,475.11877 608.22043,475.11877 C 613.54742,475.11877 618.79431,473.68691 622.10866,472.74564 L 622.10866,459.71857 C 618.8644,461.61105 615.17957,462.61241 611.11423,462.61241" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path66"/>
|
||||
<path d="M 786.85488,338.61963 L 786.85488,394.38286 L 785.63328,394.38286 L 785.47307,394.12251 C 779.53527,384.52993 769.44203,379.67351 755.47369,379.67351 C 744.8097,379.67351 735.94807,383.11801 729.14915,389.91698 C 720.8883,398.17784 716.34234,410.88448 716.34234,425.68395 C 716.34234,442.11546 721.31887,456.51439 730.00027,465.18573 C 736.58891,471.78442 745.10008,475.11877 755.29346,475.11877 C 770.87391,475.11877 781.47782,467.46874 786.20403,460.29936 L 786.36424,460.04902 L 787.56581,460.04902 L 787.56581,472.8658 L 802.62558,472.8658 L 802.62558,338.61963 L 786.85488,338.61963 z M 786.85488,427.57643 C 786.85488,438.71102 783.04989,449.15475 776.67152,455.53306 C 771.98537,460.22926 766.12768,462.61241 759.25866,462.61241 C 753.08055,462.61241 747.71351,460.3294 743.32775,455.83347 C 735.5876,447.89304 733.08431,435.35664 733.28457,427.04571 C 733.03424,415.32032 736.34859,405.06684 742.36649,398.90875 C 746.73222,394.45296 752.35961,392.18997 759.07843,392.18997 C 766.01753,392.18997 771.95533,394.60308 776.71157,399.35936 C 783.16003,405.80784 786.85488,416.09128 786.85488,427.57643" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path70"/>
|
||||
<path d="M 472.51232,338.61963 L 472.51232,394.38286 L 471.29072,394.38286 L 471.13051,394.12251 C 465.1827,384.52993 455.08945,379.67351 441.13114,379.67351 C 430.45712,379.67351 421.5955,383.11801 414.80658,389.91698 C 406.54575,398.17784 401.98976,410.88448 401.98976,425.68395 C 401.98976,442.11546 406.97631,456.51439 415.64769,465.18573 C 422.23633,471.78442 430.74751,475.11877 440.95089,475.11877 C 456.52134,475.11877 467.12525,467.46874 471.85145,460.29936 L 472.01166,460.04902 L 473.22325,460.04902 L 473.22325,472.8658 L 488.27301,472.8658 L 488.27301,338.61963 L 472.51232,338.61963 z M 472.51232,427.57643 C 472.51232,438.71102 468.69731,449.15475 462.32896,455.53306 C 457.63279,460.22926 451.7751,462.61241 444.9161,462.61241 C 438.72798,462.61241 433.37095,460.3294 428.9852,455.83347 C 421.24503,447.89304 418.73173,435.35664 418.94201,427.04571 C 418.69168,415.32032 421.99602,405.06684 428.02393,398.90875 C 432.38966,394.45296 438.00704,392.18997 444.73587,392.18997 C 451.67497,392.18997 457.60275,394.60308 462.359,399.35936 C 468.80746,405.80784 472.51232,416.09128 472.51232,427.57643" style="fill:#007cc5;fill-rule:nonzero;stroke:none" id="path74"/>
|
||||
</g>
|
||||
</g>
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70.5 33.5" style="enable-background:new 0 0 70.5 33.5;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#007DC5;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="33.5" width="70.5" x="-228.2" y="-213.4">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g>
|
||||
<path class="st0" d="M26,33.5h7.4V14.8c0-1-0.4-2-1.1-2.6l-11-11C20.7,0.4,19.7,0,18.7,0H0v7.4h20.4c3.1,0,5.6,2.5,5.6,5.6V33.5z">
|
||||
</path>
|
||||
<path class="st0" d="M44.5,33.5h-7.4V14.8c0-1,0.4-2,1.1-2.6l11-11C49.9,0.4,50.8,0,51.8,0h18.7v7.4H50.1c-3.1,0-5.6,2.5-5.6,5.6
|
||||
V33.5z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 10 KiB |
52
auth/assets/custom-icons/icons/us_mobile.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="400px" height="400px" viewBox="1754.66 549.858 400 400" enable-background="new 1754.66 549.858 400 400"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g id="deeditor_bgCarrier">
|
||||
<rect id="dee_c_e" x="1754.66" y="549.858" fill="#1043B8" width="400" height="400"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#E6E7E8" d="M2137.105,669.97l-45.542,18.7c0,0,45.358-18.52,45.358-18.701h-131.485v2.541
|
||||
c-0.184,4.357-0.918,8.534-2.204,12.347c-1.652,5.446-4.224,10.531-7.713,14.888c-4.591,6.173-10.65,11.439-17.813,14.888
|
||||
c-0.184,0-0.367,0.182-0.367,0.182c-0.367,0.18-0.734,0.362-1.286,0.544h-0.184c-0.367,0.182-0.367,0.182-0.551,0.182h-0.184
|
||||
V669.97h-50.134c-0.918,0-1.836,0.726-1.836,1.634v7.262c1.469-1.999,3.856-3.269,6.611-3.269c1.469,0,2.938,0.364,4.04,1.09
|
||||
c0.184,0,0.367,0.182,0.551,0.182c0.551,0.362,0.918,0.726,1.285,0.906c1.47,1.091,2.755,2.362,3.856,3.813l2.571,3.995
|
||||
l5.142,8.17l-13.957,21.786c-0.734-0.362-1.285-0.542-2.203-1.088c-6.945-3.615-13.023-8.695-17.813-14.888
|
||||
c-3.319-4.537-5.921-9.559-7.713-14.888c-1.12-3.853-1.858-7.806-2.203-11.803c0-1.088-0.184-2.177-0.184-3.085h-130.751
|
||||
l44.991,18.701l-45.176-18.519c1.47,5.083,3.306,9.985,5.143,14.888l35.259,14.888h-28.465c2.571,5.083,5.326,10.167,8.447,14.888
|
||||
l60.785,14.888h-50.501c4.04,5.266,8.264,10.167,12.671,14.888l78.414,15.252h-62.621c6.428,5.446,13.223,10.349,20.385,14.888
|
||||
l82.638,14.889h-54.357c20.935,8.896,43.522,13.98,67.396,14.889h14.324c23.873-0.908,46.645-6.174,67.396-14.889h-54.357
|
||||
l82.821-14.889c7.162-4.541,13.957-9.622,20.384-14.888h-62.621l78.414-14.888c4.407-4.722,8.631-9.805,12.671-14.889h-50.5
|
||||
l60.784-14.89c2.938-4.901,5.877-9.803,8.447-14.888h-28.647l35.259-14.888C2133.984,679.955,2135.637,675.054,2137.105,669.97"/>
|
||||
<path fill="#99999B" d="M1951.63,685.402c3.854,0,6.979-3.088,6.979-6.899c0-3.812-3.124-6.898-6.979-6.898
|
||||
s-6.979,3.089-6.979,6.898C1944.651,682.314,1947.777,685.402,1951.63,685.402"/>
|
||||
<path fill="#E7E7E6" d="M2005.62,669.97h-0.184v2.541c-0.184,4.357-0.918,8.534-2.204,12.347
|
||||
c-1.652,5.446-4.224,10.531-7.713,14.888c-4.591,6.173-10.65,11.439-17.813,14.888c-0.184,0-0.367,0.182-0.367,0.182
|
||||
c-0.367,0.181-0.734,0.363-1.286,0.544h-0.184c-0.367,0.182-0.367,0.182-0.551,0.182h-0.184V669.97h-50.134
|
||||
c-0.918,0-1.836,0.726-1.836,1.634v7.262c1.469-1.999,3.856-3.269,6.611-3.269c1.469,0,2.938,0.364,4.04,1.09
|
||||
c0.184,0,0.367,0.182,0.551,0.182c0.551,0.362,0.918,0.726,1.285,0.906c1.47,1.091,2.755,2.362,3.856,3.813l2.571,3.995
|
||||
l5.142,8.17l-13.957,21.786c-0.734-0.362-1.285-0.542-2.203-1.088c-6.945-3.615-13.023-8.695-17.813-14.888
|
||||
c-3.319-4.537-5.921-9.559-7.713-14.888c-1.12-3.853-1.858-7.806-2.203-11.803c0-1.088-0.184-2.177-0.184-3.085h-130.751
|
||||
l44.991,18.701l1.469,0.544l1.286,0.908l134.424,99.316l134.424-99.134l1.285-0.908l1.47-0.544c0,0,45.358-18.519,45.358-18.701
|
||||
H2005.62z M1951.813,673.057c2.388,0,4.407,1.997,4.407,4.357c0,2.361-2.02,4.357-4.407,4.357s-4.407-1.997-4.407-4.357
|
||||
C1947.406,675.054,1949.426,673.057,1951.813,673.057"/>
|
||||
<path fill="#CED4D4" d="M2137.105,669.97l-45.542,18.7l-1.47,0.544l-1.285,0.908l-134.24,99.134l-134.607-99.132l-1.286-0.908
|
||||
l-1.469-0.544l-44.992-18.701c1.47,5.083,3.306,9.985,5.143,14.888l35.259,14.888h-28.465c2.571,5.083,5.326,10.167,8.447,14.888
|
||||
l60.785,14.888h-50.501c4.04,5.266,8.264,10.167,12.671,14.888l78.414,15.252h-62.621c6.428,5.446,13.223,10.349,20.385,14.888
|
||||
l82.638,14.889h-54.357c20.935,8.896,43.522,13.98,67.396,14.889h14.324c23.873-0.908,46.645-6.174,67.396-14.889h-54.357
|
||||
l82.821-14.889c7.162-4.541,13.957-9.622,20.384-14.888h-62.621l78.414-14.888c4.407-4.722,8.631-9.805,12.671-14.889h-50.5
|
||||
l60.784-14.89c2.938-4.901,5.877-9.803,8.447-14.888h-28.647l35.259-14.888C2133.984,679.955,2135.637,675.054,2137.105,669.97"/>
|
||||
<path fill="#0055A2" d="M1970.359,726.256h-74.741l17.263,29.412h86.678l17.262-29.412H1970.359z"/>
|
||||
<path fill="#CE2832" d="M1999.559,755.668h-29.199v49.93l16.16-27.596L1999.559,755.668z"/>
|
||||
<path fill="#F2F2F2" d="M1942.08,755.668v49.93l14.141,24.331l14.141-24.331v-49.93H1942.08z"/>
|
||||
<path fill="#CE2832" d="M1940.426,755.668h-27.545L1925.919,778l16.16,27.598v-49.93H1940.426z"/>
|
||||
<path fill="#FFFFFF" d="M1956.219,732.426l2.388,4.903l5.325,0.727l-3.855,3.813l0.918,5.266l-4.774-2.542l-4.959,2.542
|
||||
l1.103-5.266l-4.041-3.813l5.51-0.727L1956.219,732.426z M1984.684,732.426l2.387,4.903l5.51,0.727l-4.04,3.813l0.918,5.266
|
||||
l-4.774-2.542l-4.774,2.542l0.918-5.266l-3.856-3.813l5.325-0.727L1984.684,732.426z M1927.571,732.426l2.571,4.903l5.325,0.727
|
||||
l-3.856,3.813l0.919,5.266l-4.959-2.542l-4.774,2.542l0.918-5.266l-3.856-3.813l5.326-0.727L1927.571,732.426z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -82,9 +82,9 @@ PODS:
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage (5.19.7):
|
||||
- SDWebImage/Core (= 5.19.7)
|
||||
- SDWebImage/Core (5.19.7)
|
||||
- SDWebImage (5.20.0):
|
||||
- SDWebImage/Core (= 5.20.0)
|
||||
- SDWebImage/Core (5.20.0)
|
||||
- Sentry/HybridSDK (8.36.0)
|
||||
- sentry_flutter (8.9.0):
|
||||
- Flutter
|
||||
@@ -245,7 +245,7 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
|
||||
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
|
||||
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
|
||||
@@ -7,8 +7,11 @@ const String sentryDSN =
|
||||
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
|
||||
const String sentryTunnel = "https://sentry-reporter.ente.io";
|
||||
const String roadmapURL = "https://roadmap.ente.io";
|
||||
const String githubIssuesUrl =
|
||||
"https://github.com/ente-io/ente/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc";
|
||||
|
||||
const String kAccountsUrl = "https://accounts.ente.io";
|
||||
|
||||
const String githubFeatureRequestUrl =
|
||||
"https://github.com/ente-io/ente/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+requests%22+label%3A%22-+auth%22+sort%3Atop";
|
||||
const int microSecondsInDay = 86400000000;
|
||||
const int android11SDKINT = 30;
|
||||
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
|
||||
|
||||
@@ -435,8 +435,6 @@
|
||||
"customEndpoint": "متصل بـ{endpoint}",
|
||||
"pinText": "ثبت",
|
||||
"unpinText": "ألغِ التثبيت",
|
||||
"pinnedCodeMessage": "ثُبِّت {code}",
|
||||
"unpinnedCodeMessage": "أُلغِي تثبيت {code}",
|
||||
"tags": "الأوسمة",
|
||||
"createNewTag": "أنشيء وسم جديد",
|
||||
"tag": "وسم",
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
"hintForMobile": "Натиснете продължително код, за да го редактирате или премахнете.",
|
||||
"hintForDesktop": "Натиснете десен бутон върху код, за да го редактирате или премахнете.",
|
||||
"scan": "Сканиране",
|
||||
"scanACode": "Скениране на код",
|
||||
"scanACode": "Сканиране на код",
|
||||
"verify": "Потвърждаване",
|
||||
"verifyEmail": "Потвърдете имейла",
|
||||
"enterCodeHint": "Въведете 6-цифрения код от\nВашето приложение за удостоверяване",
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Двуфакторно удостоверяване",
|
||||
"passkeyAuthTitle": "Удостоверяване с ключ за парола",
|
||||
"verifyPasskey": "Потвърдете ключ за парола",
|
||||
"loginWithTOTP": "Влизане с еднократен код",
|
||||
"recoverAccount": "Възстановяване на акаунт",
|
||||
"enterRecoveryKeyHint": "Въведете Вашия ключ за възстановяване",
|
||||
"recover": "Възстановяване",
|
||||
@@ -199,7 +200,7 @@
|
||||
"sorryUnableToGenCode": "За съжаление не може да се генерира код за {issuerName}",
|
||||
"noResult": "Няма резултати",
|
||||
"addCode": "Добавяне на код",
|
||||
"scanAQrCode": "Скениране на QR код",
|
||||
"scanAQrCode": "Сканиране на QR код",
|
||||
"enterDetailsManually": "Въведете подробности ръчно",
|
||||
"edit": "Редактиране",
|
||||
"share": "Споделяне",
|
||||
@@ -327,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Персонализирано",
|
||||
"editOrder": "Промяна на подредбата",
|
||||
"mostFrequentlyUsed": "Често използвани",
|
||||
"mostRecentlyUsed": "Последно използвани",
|
||||
"activeSessions": "Активни сесии",
|
||||
"somethingWentWrongPleaseTryAgain": "Нещо се обърка, моля опитайте отново",
|
||||
"thisWillLogYouOutOfThisDevice": "Това ще Ви изкара от профила на това устройство!",
|
||||
@@ -444,10 +449,11 @@
|
||||
"invalidEndpointMessage": "За съжаление въведената от Вас крайна точка е невалидна. Моля, въведете валидна крайна точка и опитайте отново.",
|
||||
"endpointUpdatedMessage": "Крайната точка е актуализирана успешно",
|
||||
"customEndpoint": "Свързан към {endpoint}",
|
||||
"pinText": "ПИН код",
|
||||
"pinText": "Закачане",
|
||||
"unpinText": "Откачане",
|
||||
"pinnedCodeMessage": "{code} е закачен",
|
||||
"unpinnedCodeMessage": "{code} е откачен",
|
||||
"pinned": "Закачен",
|
||||
"tags": "Етикети",
|
||||
"createNewTag": "Създаване на етикет",
|
||||
"tag": "Етикет",
|
||||
|
||||
@@ -446,8 +446,6 @@
|
||||
"customEndpoint": "Connectat a {endpoint}",
|
||||
"pinText": "Fixa",
|
||||
"unpinText": "Desfixa",
|
||||
"pinnedCodeMessage": "{code} fixat",
|
||||
"unpinnedCodeMessage": "{code} deixat de fixar",
|
||||
"tags": "Etiquetes",
|
||||
"createNewTag": "Crea una nova etiqueta",
|
||||
"tag": "Etiqueta",
|
||||
|
||||
@@ -446,8 +446,6 @@
|
||||
"customEndpoint": "Forbindelse oprettet til {endpoint}",
|
||||
"pinText": "Fastgør",
|
||||
"unpinText": "Frigør",
|
||||
"pinnedCodeMessage": "{code} er blevet fastgjort",
|
||||
"unpinnedCodeMessage": "{code} er blevet frigjort",
|
||||
"tags": "Tags",
|
||||
"createNewTag": "Opret nyt tag",
|
||||
"tag": "Tag",
|
||||
|
||||
@@ -444,8 +444,6 @@
|
||||
"customEndpoint": "Mit {endpoint} verbunden",
|
||||
"pinText": "Anpinnen",
|
||||
"unpinText": "Lösen",
|
||||
"pinnedCodeMessage": "{code} wurde angepinnt",
|
||||
"unpinnedCodeMessage": "{code} wurde Losgelöst",
|
||||
"tags": "Tags",
|
||||
"createNewTag": "Neuen Tag erstellen",
|
||||
"tag": "Tag",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Αυθεντικοποίηση δύο παραγόντων",
|
||||
"passkeyAuthTitle": "Επιβεβαίωση κλειδιού πρόσβασης",
|
||||
"verifyPasskey": "Επιβεβαίωση κλειδιού πρόσβασης",
|
||||
"loginWithTOTP": "Είσοδος με TOTP",
|
||||
"recoverAccount": "Ανάκτηση λογαριασμού",
|
||||
"enterRecoveryKeyHint": "Εισάγετε το κλειδί ανάκτησης σας",
|
||||
"recover": "Ανάκτηση",
|
||||
@@ -327,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Προσαρμοσμένο",
|
||||
"editOrder": "Επεξεργασία σειράς",
|
||||
"mostFrequentlyUsed": "Συχνά χρησιμοποιούμενο",
|
||||
"mostRecentlyUsed": "Πρόσφατα χρησιμοποιούμενο",
|
||||
"activeSessions": "Ενεργές συνεδρίες",
|
||||
"somethingWentWrongPleaseTryAgain": "Κάτι πήγε στραβά, παρακαλώ προσπαθήστε ξανά",
|
||||
"thisWillLogYouOutOfThisDevice": "Αυτό θα σας αποσυνδέσει από αυτή τη συσκευή!",
|
||||
@@ -446,8 +451,9 @@
|
||||
"customEndpoint": "Συνδεδεμένο στο {endpoint}",
|
||||
"pinText": "Καρφίτσωμα",
|
||||
"unpinText": "Ξεκαρφίτσωμα",
|
||||
"pinnedCodeMessage": "Το {code} καρφιτσώθηκε",
|
||||
"unpinnedCodeMessage": "Το {code} ξεκαρφιτσώθηκε",
|
||||
"pinnedCodeMessage": "{code} έχει καρφιτσωθεί",
|
||||
"unpinnedCodeMessage": "Το {code} έχει ξεκαρφιτσωθεί",
|
||||
"pinned": "Καρφιτσωμένο",
|
||||
"tags": "Ετικέτες",
|
||||
"createNewTag": "Δημιουργία Νέας Ετικέτας",
|
||||
"tag": "Ετικέτα",
|
||||
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Are you sure you want to logout?",
|
||||
"yesLogout": "Yes, logout",
|
||||
"exit": "Exit",
|
||||
"theme": "Theme",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"systemTheme": "System",
|
||||
"verifyingRecoveryKey": "Verifying recovery key...",
|
||||
"recoveryKeyVerified": "Recovery key verified",
|
||||
"recoveryKeySuccessBody": "Great! Your recovery key is valid. Thank you for verifying.\n\nPlease remember to keep your recovery key safely backed up.",
|
||||
@@ -454,7 +458,6 @@
|
||||
"pinnedCodeMessage": "{code} has been pinned",
|
||||
"unpinnedCodeMessage": "{code} has been unpinned",
|
||||
"pinned": "Pinned",
|
||||
|
||||
"tags": "Tags",
|
||||
"createNewTag": "Create New Tag",
|
||||
"tag": "Tag",
|
||||
@@ -491,5 +494,13 @@
|
||||
"appLockNotEnabled": "App lock not enabled",
|
||||
"appLockNotEnabledDescription": "Please enable app lock from Security > App Lock",
|
||||
"authToViewPasskey": "Please authenticate to view passkey",
|
||||
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data."
|
||||
"appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.",
|
||||
"duplicateCodes": "Duplicate codes",
|
||||
"noDuplicates": "✨ No duplicates",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "You've no duplicate codes that can be cleared",
|
||||
"deduplicateCodes": "Deduplicate codes",
|
||||
"deselectAll": "Deselect all",
|
||||
"selectAll": "Select all",
|
||||
"deleteDuplicates": "Delete duplicates",
|
||||
"plainHTML": "Plain HTML"
|
||||
}
|
||||
@@ -54,7 +54,7 @@
|
||||
"viewLogsAction": "Ver Registros",
|
||||
"sendLogsDescription": "Esto enviará registros para ayudarnos a depurar su problema. Aunque tomamos precauciones para asegurarnos que no se registre información sensible, le recomendamos que consulte estos registros antes de compartirlos.",
|
||||
"preparingLogsTitle": "Preparando registros...",
|
||||
"emailLogsTitle": "Enviar registros por email",
|
||||
"emailLogsTitle": "Enviar registros por correo electrónico",
|
||||
"emailLogsMessage": "Por favor, envíe los registros a {email}",
|
||||
"@emailLogsMessage": {
|
||||
"placeholders": {
|
||||
@@ -115,7 +115,7 @@
|
||||
"importCodeDelimiterInfo": "Los códigos pueden separarse por una coma o una nueva línea",
|
||||
"selectFile": "Seleccionar archivo",
|
||||
"emailVerificationToggle": "Verificación de correo electrónico",
|
||||
"emailVerificationEnableWarning": "Para evitar quedarte bloqueado fuera de tu cuenta, asegúrate de guardar una copia de su código 2FA de tu correo electrónico fuera de Ente Auth antes de habilitar la verificación de correo electrónico.",
|
||||
"emailVerificationEnableWarning": "Para evitar quedarte bloqueado fuera de tu cuenta, asegúrate de guardar una copia de tu código 2FA de tu correo electrónico fuera de Ente Auth antes de habilitar la verificación de correo electrónico.",
|
||||
"authToChangeEmailVerificationSetting": "Por favor, autentícate para cambiar tu correo electrónico",
|
||||
"authenticateGeneric": "Por favor, autentícate",
|
||||
"authToViewYourRecoveryKey": "Por favor, autentícate para ver tu clave de recuperación",
|
||||
@@ -160,7 +160,7 @@
|
||||
"recoverAccount": "Recuperar cuenta",
|
||||
"enterRecoveryKeyHint": "Introduce tu clave de recuperación",
|
||||
"recover": "Recuperar",
|
||||
"contactSupportViaEmailMessage": "Por favor, envía un email a {email} desde la dirección de correo electrónico que usó durante el registro",
|
||||
"contactSupportViaEmailMessage": "Por favor, envía un correo electrónico a {email} desde la dirección de correo electrónico que usó durante el registro",
|
||||
"@contactSupportViaEmailMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "¿Seguro que quieres cerrar la sesión?",
|
||||
"yesLogout": "Sí, cerrar la sesión",
|
||||
"exit": "Salir",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Claro",
|
||||
"darkTheme": "Oscuro",
|
||||
"systemTheme": "Sistema",
|
||||
"verifyingRecoveryKey": "Verificando clave de recuperación...",
|
||||
"recoveryKeyVerified": "Clave de recuperación verificada",
|
||||
"recoveryKeySuccessBody": "¡Genial! Su clave de recuperación es válida. Gracias por verificar.\n\nPor favor, recuerde mantener su clave de recuperación segura.",
|
||||
@@ -317,7 +321,7 @@
|
||||
"checkInboxAndSpamFolder": "Por favor revisa tu bandeja de entrada (y spam) para completar la verificación",
|
||||
"tapToEnterCode": "Toca para introducir el código",
|
||||
"resendEmail": "Reenviar correo electrónico",
|
||||
"weHaveSendEmailTo": "Hemos enviado un correo a <green>{email}</green>",
|
||||
"weHaveSendEmailTo": "Hemos enviado un correo electrónico a <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Personalizado",
|
||||
"editOrder": "Editar orden",
|
||||
"mostFrequentlyUsed": "Usados frecuentemente",
|
||||
"mostRecentlyUsed": "Usados recientemente",
|
||||
"activeSessions": "Sesiones activas",
|
||||
"somethingWentWrongPleaseTryAgain": "Algo ha ido mal, por favor, inténtelo de nuevo",
|
||||
"thisWillLogYouOutOfThisDevice": "¡Esto cerrará la sesión de este dispositivo!",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Desanclar",
|
||||
"pinnedCodeMessage": "{code} ha sido anclado",
|
||||
"unpinnedCodeMessage": "{code} ha sido desanclado",
|
||||
"pinned": "Anclado",
|
||||
"tags": "Etiquetas",
|
||||
"createNewTag": "Crear Nueva Etiqueta",
|
||||
"tag": "Etiqueta",
|
||||
@@ -485,5 +494,12 @@
|
||||
"appLockNotEnabled": "Bloqueo de aplicación no activado",
|
||||
"appLockNotEnabledDescription": "Por favor, activa el bloqueo de aplicación desde Seguridad > Bloqueo de aplicación",
|
||||
"authToViewPasskey": "Por favor, autentícate para ver tu clave de acceso",
|
||||
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos."
|
||||
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.",
|
||||
"duplicateCodes": "Duplicar códigos",
|
||||
"noDuplicates": "✨ No hay duplicados",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar",
|
||||
"deduplicateCodes": "Desduplicar códigos",
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"deleteDuplicates": "Eliminar duplicados"
|
||||
}
|
||||
@@ -401,8 +401,6 @@
|
||||
"customEndpoint": "متصل شده به {endpoint}",
|
||||
"pinText": "پین",
|
||||
"unpinText": "حذف پین",
|
||||
"pinnedCodeMessage": "{code} پین شد",
|
||||
"unpinnedCodeMessage": "{code} از پین حذف شد",
|
||||
"tags": "برچسبها",
|
||||
"createNewTag": "ایجاد برچسب جدید",
|
||||
"tag": "برچسب",
|
||||
|
||||
@@ -445,8 +445,6 @@
|
||||
"customEndpoint": "Connecté à {endpoint}",
|
||||
"pinText": "Épingler",
|
||||
"unpinText": "Désépingler",
|
||||
"pinnedCodeMessage": "{code} a été épinglé",
|
||||
"unpinnedCodeMessage": "{code} a été désépinglé",
|
||||
"tags": "Tags",
|
||||
"createNewTag": "Créer un nouveau tag",
|
||||
"tag": "Tag",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Autentikasi dua langkah",
|
||||
"passkeyAuthTitle": "Verifikasi passkey",
|
||||
"verifyPasskey": "Verifikasi passkey",
|
||||
"loginWithTOTP": "Login menggunakan TOTP",
|
||||
"recoverAccount": "Pulihkan akun",
|
||||
"enterRecoveryKeyHint": "Masukkan kunci pemulihanmu",
|
||||
"recover": "Pulihkan",
|
||||
@@ -327,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Kustom",
|
||||
"editOrder": "Ubah pesanan",
|
||||
"mostFrequentlyUsed": "Sering digunakan",
|
||||
"mostRecentlyUsed": "Baru digunakan",
|
||||
"activeSessions": "Sesi aktif",
|
||||
"somethingWentWrongPleaseTryAgain": "Ada yang salah. Mohon coba kembali",
|
||||
"thisWillLogYouOutOfThisDevice": "Langkah ini akan mengeluarkan Anda dari gawai ini!",
|
||||
@@ -446,8 +451,6 @@
|
||||
"customEndpoint": "Terkoneksi ke {endpoint}",
|
||||
"pinText": "Sematkan",
|
||||
"unpinText": "Awasematkan",
|
||||
"pinnedCodeMessage": "{code} telah disematkan",
|
||||
"unpinnedCodeMessage": "{code} telah diawasematkan",
|
||||
"tags": "Tanda",
|
||||
"createNewTag": "Buat Tanda Baru",
|
||||
"tag": "Tanda",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Autenticazione a due fattori",
|
||||
"passkeyAuthTitle": "Verifica della passkey",
|
||||
"verifyPasskey": "Verifica passkey",
|
||||
"loginWithTOTP": "Login con TOTP",
|
||||
"recoverAccount": "Recupera account",
|
||||
"enterRecoveryKeyHint": "Inserisci la tua chiave di recupero",
|
||||
"recover": "Recupera",
|
||||
@@ -327,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Personalizzato",
|
||||
"editOrder": "Modifica ordine",
|
||||
"mostFrequentlyUsed": "Utilizzato di frequente",
|
||||
"mostRecentlyUsed": "Utilizzato di recente",
|
||||
"activeSessions": "Sessioni attive",
|
||||
"somethingWentWrongPleaseTryAgain": "Qualcosa è andato storto, per favore riprova",
|
||||
"thisWillLogYouOutOfThisDevice": "Questo ti disconnetterà da questo dispositivo!",
|
||||
@@ -448,6 +453,7 @@
|
||||
"unpinText": "Sgancia",
|
||||
"pinnedCodeMessage": "{code} è stato fissato",
|
||||
"unpinnedCodeMessage": "{code} è stato sganciato",
|
||||
"pinned": "Fissato",
|
||||
"tags": "Tag",
|
||||
"createNewTag": "Crea un nuovo tag",
|
||||
"tag": "Tag",
|
||||
@@ -484,5 +490,12 @@
|
||||
"appLockNotEnabled": "Blocco app non abilitato",
|
||||
"appLockNotEnabledDescription": "Si prega di abilitare il blocco dell'app da Sicurezza > Blocco App",
|
||||
"authToViewPasskey": "Autenticati per visualizzare le tue passkey",
|
||||
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati."
|
||||
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.",
|
||||
"duplicateCodes": "Codici duplicati",
|
||||
"noDuplicates": "✨ Nessun doppione",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati",
|
||||
"deduplicateCodes": "Codici deduplicati",
|
||||
"deselectAll": "Deselezionare tutti",
|
||||
"selectAll": "Seleziona tutti",
|
||||
"deleteDuplicates": "Elimina i duplicati"
|
||||
}
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "2 要素認証",
|
||||
"passkeyAuthTitle": "パスキー認証",
|
||||
"verifyPasskey": "パスキーの認証",
|
||||
"loginWithTOTP": "TOTPでログイン",
|
||||
"recoverAccount": "アカウントを回復",
|
||||
"enterRecoveryKeyHint": "回復キーを入力",
|
||||
"recover": "回復",
|
||||
@@ -327,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "カスタム",
|
||||
"editOrder": "並べ替え",
|
||||
"mostFrequentlyUsed": "よく使う",
|
||||
"mostRecentlyUsed": "最近使った",
|
||||
"activeSessions": "アクティブセッション",
|
||||
"somethingWentWrongPleaseTryAgain": "問題が発生しました、再試行してください",
|
||||
"thisWillLogYouOutOfThisDevice": "このデバイスからログアウトします!",
|
||||
@@ -446,8 +451,9 @@
|
||||
"customEndpoint": "{endpoint} に接続しました",
|
||||
"pinText": "固定",
|
||||
"unpinText": "固定を解除",
|
||||
"pinnedCodeMessage": "{code} を固定しました",
|
||||
"unpinnedCodeMessage": "{code} の固定が解除されました",
|
||||
"pinnedCodeMessage": "{code}がピン留めされました",
|
||||
"unpinnedCodeMessage": "{code}のピン留めが解除されました",
|
||||
"pinned": "ピン留め",
|
||||
"tags": "タグ",
|
||||
"createNewTag": "新しいタグの作成",
|
||||
"tag": "タグ",
|
||||
|
||||
@@ -328,6 +328,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "사용자 정의",
|
||||
"editOrder": "순서 변경",
|
||||
"mostFrequentlyUsed": "자주 사용됨",
|
||||
"mostRecentlyUsed": "최근에 사용됨",
|
||||
"activeSessions": "활성화된 세션",
|
||||
"somethingWentWrongPleaseTryAgain": "뭔가 잘못됐습니다, 다시 시도해주세요",
|
||||
"thisWillLogYouOutOfThisDevice": "이 작업을 하시면 기기에서 로그아웃하게 됩니다!",
|
||||
@@ -449,6 +453,7 @@
|
||||
"unpinText": "핀 해제",
|
||||
"pinnedCodeMessage": "{code}가 핀 되었습니다.",
|
||||
"unpinnedCodeMessage": "{code}의 핀이 해제되었습니다.",
|
||||
"pinned": "고정됨",
|
||||
"tags": "태그",
|
||||
"createNewTag": "새 태그 만들기",
|
||||
"tag": "태그",
|
||||
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Ar tikrai norite atsijungti?",
|
||||
"yesLogout": "Taip, atsijungti",
|
||||
"exit": "Išeiti",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Šviesi",
|
||||
"darkTheme": "Tamsi",
|
||||
"systemTheme": "Sistemos",
|
||||
"verifyingRecoveryKey": "Patvirtinima atkūrimo raktą...",
|
||||
"recoveryKeyVerified": "Patvirtintas atkūrimo raktas",
|
||||
"recoveryKeySuccessBody": "Puiku! Jūsų atkūrimo raktas tinkamas. Dėkojame už patvirtinimą.\n\nNepamirškite sukurti saugią atkūrimo rakto atsarginę kopiją.",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Pasirinktinis",
|
||||
"editOrder": "Redaguoti tvarką",
|
||||
"mostFrequentlyUsed": "Dažniausiai naudojamą",
|
||||
"mostRecentlyUsed": "Neseniai naudotą",
|
||||
"activeSessions": "Aktyvūs seansai",
|
||||
"somethingWentWrongPleaseTryAgain": "Kažkas nutiko ne taip. Bandykite dar kartą.",
|
||||
"thisWillLogYouOutOfThisDevice": "Tai jus atjungs nuo šio įrenginio.",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Atsegti",
|
||||
"pinnedCodeMessage": "{code} buvo prisegtas",
|
||||
"unpinnedCodeMessage": "{code} buvo atsegtas",
|
||||
"pinned": "Prisegta",
|
||||
"tags": "Žymės",
|
||||
"createNewTag": "Kurti naują žymę",
|
||||
"tag": "Žymė",
|
||||
@@ -485,5 +494,13 @@
|
||||
"appLockNotEnabled": "Programos užraktas neįjungtas",
|
||||
"appLockNotEnabledDescription": "Įjunkite programos užraktą iš Saugumas > Programos užraktas",
|
||||
"authToViewPasskey": "Nustatykite tapatybę, kad peržiūrėtumėte slaptaraktį",
|
||||
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų."
|
||||
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.",
|
||||
"duplicateCodes": "Dubliuoti kodus",
|
||||
"noDuplicates": "✨ Dublikatų nėra",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.",
|
||||
"deduplicateCodes": "Atdubliuoti kodus",
|
||||
"deselectAll": "Naikinti visų pasirinkimą",
|
||||
"selectAll": "Pasirinkti viską",
|
||||
"deleteDuplicates": "Ištrinti dublikatus",
|
||||
"plainHTML": "Grynasis HTML"
|
||||
}
|
||||
1
auth/lib/l10n/arb/app_ml.arb
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Tweestapsverificatie",
|
||||
"passkeyAuthTitle": "Passkey verificatie",
|
||||
"verifyPasskey": "Bevestig passkey",
|
||||
"loginWithTOTP": "Inloggen met TOTP",
|
||||
"recoverAccount": "Account herstellen",
|
||||
"enterRecoveryKeyHint": "Voer je herstelsleutel in",
|
||||
"recover": "Herstellen",
|
||||
@@ -257,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Weet je zeker dat je wilt uitloggen?",
|
||||
"yesLogout": "Ja, uitloggen",
|
||||
"exit": "Afsluiten",
|
||||
"theme": "Thema",
|
||||
"lightTheme": "Licht",
|
||||
"darkTheme": "Donker",
|
||||
"systemTheme": "Systeem",
|
||||
"verifyingRecoveryKey": "Herstelsleutel verifiëren...",
|
||||
"recoveryKeyVerified": "Herstelsleutel geverifieerd",
|
||||
"recoveryKeySuccessBody": "Super! Je herstelsleutel is geldig. Bedankt voor het verifiëren.\n\nVergeet niet om je herstelsleutel veilig te bewaren.",
|
||||
@@ -327,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Aangepast",
|
||||
"editOrder": "Volgorde wijzigen",
|
||||
"mostFrequentlyUsed": "Vaak gebruikt",
|
||||
"mostRecentlyUsed": "Recent gebruikt",
|
||||
"activeSessions": "Actieve sessies",
|
||||
"somethingWentWrongPleaseTryAgain": "Er is iets fout gegaan, probeer het opnieuw",
|
||||
"thisWillLogYouOutOfThisDevice": "Dit zal je uitloggen van dit apparaat!",
|
||||
@@ -448,6 +457,7 @@
|
||||
"unpinText": "Losmaken",
|
||||
"pinnedCodeMessage": "{code} is vastgezet",
|
||||
"unpinnedCodeMessage": "{code} is losgemaakt",
|
||||
"pinned": "Vastgezet",
|
||||
"tags": "Labels",
|
||||
"createNewTag": "Nieuw label maken",
|
||||
"tag": "Label",
|
||||
@@ -484,5 +494,12 @@
|
||||
"appLockNotEnabled": "App-vergrendeling niet ingeschakeld",
|
||||
"appLockNotEnabledDescription": "Schakel app vergrendeling in vanuit Beveiliging > App vergrendeling",
|
||||
"authToViewPasskey": "Verifieer uzelf om uw passkey te bekijken",
|
||||
"appLockOfflineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder backups. Als je jouw applock vergeet, wordt je uitgesloten van toegang tot je gegevens."
|
||||
"appLockOfflineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder backups. Als je jouw applock vergeet, wordt je uitgesloten van toegang tot je gegevens.",
|
||||
"duplicateCodes": "Dubbele codes",
|
||||
"noDuplicates": "✨ Geen dubbele",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Je hebt geen dubbele codes die kunnen worden gewist",
|
||||
"deduplicateCodes": "Dubbele codes",
|
||||
"deselectAll": "Alles deselecteren",
|
||||
"selectAll": "Alles selecteren",
|
||||
"deleteDuplicates": "Dubbelen verwijderen"
|
||||
}
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Czy na pewno chcesz się wylogować?",
|
||||
"yesLogout": "Tak, wyloguj",
|
||||
"exit": "Wyjdź",
|
||||
"theme": "Motyw",
|
||||
"lightTheme": "Jasny",
|
||||
"darkTheme": "Ciemny",
|
||||
"systemTheme": "Systemowy",
|
||||
"verifyingRecoveryKey": "Weryfikowanie klucza odzyskiwania...",
|
||||
"recoveryKeyVerified": "Klucz odzyskiwania zweryfikowany",
|
||||
"recoveryKeySuccessBody": "Znakomicie! Klucz odzyskiwania jest prawidłowy. Dziękujemy za weryfikację.\n\nPamiętaj, aby bezpiecznie przechowywać kopię zapasową klucza odzyskiwania.",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Niestandardowe",
|
||||
"editOrder": "Zmień kolejność",
|
||||
"mostFrequentlyUsed": "Często używane",
|
||||
"mostRecentlyUsed": "Ostatnio używane",
|
||||
"activeSessions": "Aktywne sesje",
|
||||
"somethingWentWrongPleaseTryAgain": "Coś poszło nie tak, spróbuj ponownie",
|
||||
"thisWillLogYouOutOfThisDevice": "To wyloguje Cię z tego urządzenia!",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Odepnij",
|
||||
"pinnedCodeMessage": "Przypięto {code}",
|
||||
"unpinnedCodeMessage": "Odpięto {code}",
|
||||
"pinned": "Przypięte",
|
||||
"tags": "Etykiety",
|
||||
"createNewTag": "Utwórz nową etykietę",
|
||||
"tag": "Etykieta",
|
||||
@@ -485,5 +494,13 @@
|
||||
"appLockNotEnabled": "Blokada aplikacji nie jest włączona",
|
||||
"appLockNotEnabledDescription": "Prosimy włączyć blokadę aplikacji z Zabezpieczenia > Blokada aplikacji",
|
||||
"authToViewPasskey": "Prosimy uwierzytelnić się, aby wyświetlić klucz dostępu",
|
||||
"appLockOfflineModeWarning": "Wybrano kontynuowanie bez kopii zapasowych. Jeśli zapomnisz blokady aplikacji, utracisz dostęp do swoich danych."
|
||||
"appLockOfflineModeWarning": "Wybrano kontynuowanie bez kopii zapasowych. Jeśli zapomnisz blokady aplikacji, utracisz dostęp do swoich danych.",
|
||||
"duplicateCodes": "Duplikuj kody",
|
||||
"noDuplicates": "✨ Brak duplikatów",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Nie masz duplikatów kodów, które mogą być wyczyszczone",
|
||||
"deduplicateCodes": "Deduplikuj kody",
|
||||
"deselectAll": "Odznacz wszystko",
|
||||
"selectAll": "Zaznacz wszystko",
|
||||
"deleteDuplicates": "Usuń duplikaty",
|
||||
"plainHTML": "Zwykły HTML"
|
||||
}
|
||||
@@ -132,7 +132,7 @@
|
||||
"general": "Geral",
|
||||
"settings": "Ajustes",
|
||||
"copied": "Copiado",
|
||||
"pleaseTryAgain": "Tente de novo",
|
||||
"pleaseTryAgain": "Tente novamente",
|
||||
"existingUser": "Usuário existente",
|
||||
"newUser": "Novo no Ente",
|
||||
"delete": "Excluir",
|
||||
@@ -142,7 +142,7 @@
|
||||
"suggestFeatures": "Sugerir recursos",
|
||||
"faq": "Perguntas frequentes",
|
||||
"somethingWentWrongMessage": "Algo deu errado. Tente outra vez",
|
||||
"leaveFamily": "Sair da família",
|
||||
"leaveFamily": "Sair do plano familiar",
|
||||
"leaveFamilyMessage": "Deseja mesmo sair do plano familiar?",
|
||||
"inFamilyPlanMessage": "Você está em um plano familiar!",
|
||||
"hintForMobile": "Pressione em um código para editar ou excluir.",
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Deseja mesmo sair?",
|
||||
"yesLogout": "Sim, quero sair",
|
||||
"exit": "Sair",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Claro",
|
||||
"darkTheme": "Escuro",
|
||||
"systemTheme": "Sistema",
|
||||
"verifyingRecoveryKey": "Verificando chave de recuperação...",
|
||||
"recoveryKeyVerified": "Chave de recuperação verificada",
|
||||
"recoveryKeySuccessBody": "Ótimo! Sua chave de recuperação é válida. Obrigada por verificar.\n\nLembre-se de manter sua chave de recuperação copiada com segurança.",
|
||||
@@ -271,7 +275,7 @@
|
||||
"recoveryKeyVerifyReason": "Sua chave de recuperação é a única maneira de recuperar suas fotos se você esqueceu sua senha. Você pode encontrar sua chave de recuperação em Opções > Conta.\n\nInsira sua chave de recuperação aqui para verificar se você a salvou corretamente.",
|
||||
"confirmYourRecoveryKey": "Confirme sua chave de recuperação",
|
||||
"confirm": "Confirmar",
|
||||
"emailYourLogs": "Enviar logs por e-mail",
|
||||
"emailYourLogs": "Enviar registros por e-mail",
|
||||
"pleaseSendTheLogsTo": "Envie os logs para \n{toEmail}",
|
||||
"copyEmailAddress": "Copiar endereço de e-mail",
|
||||
"exportLogs": "Exportar logs",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Personalizado",
|
||||
"editOrder": "Editar ordem",
|
||||
"mostFrequentlyUsed": "Usado com frequência",
|
||||
"mostRecentlyUsed": "Usado recentemente",
|
||||
"activeSessions": "Sessões ativas",
|
||||
"somethingWentWrongPleaseTryAgain": "Algo deu errado. Tente outra vez",
|
||||
"thisWillLogYouOutOfThisDevice": "Isso fará com que você saia deste dispositivo!",
|
||||
@@ -337,7 +345,7 @@
|
||||
"thisDevice": "Esse dispositivo",
|
||||
"toResetVerifyEmail": "Para redefinir sua senha, verifique seu e-mail primeiramente.",
|
||||
"thisEmailIsAlreadyInUse": "Este e-mail já está em uso",
|
||||
"verificationFailedPleaseTryAgain": "Falha na verificação. Tente novamente",
|
||||
"verificationFailedPleaseTryAgain": "Falhou na verificação. Tente novamente",
|
||||
"yourVerificationCodeHasExpired": "Seu código de verificação expirou",
|
||||
"incorrectCode": "Código incorreto",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "O código inserido está incorreto",
|
||||
@@ -354,7 +362,7 @@
|
||||
"plainText": "Texto simples",
|
||||
"passwordToEncryptExport": "Senha para criptografar a exportação",
|
||||
"export": "Exportar",
|
||||
"useOffline": "Usar sem backups",
|
||||
"useOffline": "Usar sem cópia de segurança",
|
||||
"signInToBackup": "Entre para fazer backup de seus códigos",
|
||||
"singIn": "Entrar",
|
||||
"sigInBackupReminder": "Exporte seus códigos para garantir que você tenha uma cópia para restaurar.",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Desafixar",
|
||||
"pinnedCodeMessage": "{code} foi fixado",
|
||||
"unpinnedCodeMessage": "{code} foi desafixado",
|
||||
"pinned": "Fixado",
|
||||
"tags": "Etiquetas",
|
||||
"createNewTag": "Criar nova etiqueta",
|
||||
"tag": "Etiqueta",
|
||||
@@ -485,5 +494,13 @@
|
||||
"appLockNotEnabled": "Bloqueio de aplicativo não ativado",
|
||||
"appLockNotEnabledDescription": "Ative o bloqueio de aplicativo em Segurança > Bloqueio de aplicativo",
|
||||
"authToViewPasskey": "Autentique para ver a sua chave de acesso",
|
||||
"appLockOfflineModeWarning": "Você prosseguiu sem cópias de segurança. Caso, se esqueça de seu aplicativo de bloqueio, você não poderá mais acessar seus dados."
|
||||
"appLockOfflineModeWarning": "Você prosseguiu sem cópias de segurança. Caso, se esqueça de seu aplicativo de bloqueio, você não poderá mais acessar seus dados.",
|
||||
"duplicateCodes": "Duplicar códigos",
|
||||
"noDuplicates": "✨ Sem duplicados",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Você não possui códigos duplicados para limpar",
|
||||
"deduplicateCodes": "Desduplicar códigos",
|
||||
"deselectAll": "Deselecionar tudo",
|
||||
"selectAll": "Selecionar tudo",
|
||||
"deleteDuplicates": "Excluir duplicados",
|
||||
"plainHTML": "HTML simples"
|
||||
}
|
||||
@@ -446,8 +446,6 @@
|
||||
"customEndpoint": "Подключено к {endpoint}",
|
||||
"pinText": "Прикрепить",
|
||||
"unpinText": "Открепить",
|
||||
"pinnedCodeMessage": "{code} прикреплен",
|
||||
"unpinnedCodeMessage": "{code} откреплен",
|
||||
"tags": "Метки",
|
||||
"createNewTag": "Создать новую метку",
|
||||
"tag": "Метка",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"timeBasedKeyType": "Na základe času (TOTP)",
|
||||
"counterBasedKeyType": "Na základe počítadla (HOTP)",
|
||||
"saveAction": "Uložiť",
|
||||
"nextTotpTitle": "ďalej",
|
||||
"nextTotpTitle": "ďalší",
|
||||
"deleteCodeTitle": "Odstrániť položku?",
|
||||
"deleteCodeMessage": "Naozaj chcete odstrániť položku? Táto akcia je nezvratná.",
|
||||
"trashCode": "Odstrániť kód?",
|
||||
@@ -156,6 +156,7 @@
|
||||
"twoFactorAuthTitle": "Dvojfaktorové overovanie",
|
||||
"passkeyAuthTitle": "Overenie pomocou passkey",
|
||||
"verifyPasskey": "Overiť passkey",
|
||||
"loginWithTOTP": "Prihlásenie pomocou TOTP",
|
||||
"recoverAccount": "Obnoviť účet",
|
||||
"enterRecoveryKeyHint": "Vložte váš kód pre obnovenie",
|
||||
"recover": "Obnoviť",
|
||||
@@ -446,8 +447,6 @@
|
||||
"customEndpoint": "Pripojený k endpointu {endpoint}",
|
||||
"pinText": "Pripnúť",
|
||||
"unpinText": "Odopnúť",
|
||||
"pinnedCodeMessage": "{code} bol pripnutý",
|
||||
"unpinnedCodeMessage": "{code} bol odopnutý",
|
||||
"tags": "Tagy",
|
||||
"createNewTag": "Vytvoriť nový tag",
|
||||
"tag": "Tag",
|
||||
|
||||
@@ -327,6 +327,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mostFrequentlyUsed": "Pogosto uporabljeni",
|
||||
"mostRecentlyUsed": "Nedavno uporabljeno",
|
||||
"activeSessions": "Aktivne seje",
|
||||
"somethingWentWrongPleaseTryAgain": "Nekaj je šlo narobe, prosimo poizkusite znova.",
|
||||
"thisWillLogYouOutOfThisDevice": "To vas bo odjavilo iz te naprave!",
|
||||
@@ -446,8 +448,7 @@
|
||||
"customEndpoint": "Povezano na {endpoint}",
|
||||
"pinText": "Pripni",
|
||||
"unpinText": "Odpni",
|
||||
"pinnedCodeMessage": "{code} je bila pripeta",
|
||||
"unpinnedCodeMessage": "{code} je bila odpeta",
|
||||
"pinned": "Pripeto",
|
||||
"tags": "Oznake",
|
||||
"createNewTag": "Ustvari novo oznako",
|
||||
"tag": "Oznaka",
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"account": "Konto",
|
||||
"unlock": "Lås upp",
|
||||
"recoveryKey": "Återställningsnyckel",
|
||||
"counterAppBarTitle": "Räknare",
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "Säkerhetskopiera dina 2FA-koder",
|
||||
"onBoardingGetStarted": "Kom igång",
|
||||
"setupFirstAccount": "Konfigurera ditt första konto",
|
||||
@@ -15,22 +19,41 @@
|
||||
"pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen",
|
||||
"codeIssuerHint": "Utfärdare",
|
||||
"codeSecretKeyHint": "Secret Key",
|
||||
"secret": "Säkerhets nyckel",
|
||||
"all": "Alla",
|
||||
"notes": "Anteckningar",
|
||||
"notesLengthLimit": "Anteckningar kan vara högst {count} tecken långa",
|
||||
"@notesLengthLimit": {
|
||||
"description": "Text to indicate the maximum number of characters allowed for notes",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"description": "The maximum number of characters allowed for notes",
|
||||
"type": "int",
|
||||
"example": "100"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codeAccountHint": "Konto (du@domän.com)",
|
||||
"codeTagHint": "Tagg",
|
||||
"accountKeyType": "Typ av nyckel",
|
||||
"sessionExpired": "Sessionen har gått ut",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
},
|
||||
"pleaseLoginAgain": "Logga in igen",
|
||||
"loggingOut": "Loggar ut...",
|
||||
"timeBasedKeyType": "Tidsbaserad (TOTP)",
|
||||
"counterBasedKeyType": "Räknarbaserad (HOTP)",
|
||||
"saveAction": "Spara",
|
||||
"nextTotpTitle": "nästa",
|
||||
"deleteCodeTitle": "Radera kod?",
|
||||
"deleteCodeMessage": "Vill du ta bort den här koden? Det går inte att ångra den här åtgärden.",
|
||||
"trashCode": "Ta bort kod?",
|
||||
"trashCodeMessage": "Är du säker på att du vill ta bort koden för {account}?",
|
||||
"trash": "Papperskorg",
|
||||
"viewLogsAction": "Visa loggar",
|
||||
"sendLogsDescription": "Detta kommer att skicka över loggar för att hjälpa oss felsöka ditt problem. Även om vi vidtar försiktighetsåtgärder för att säkerställa att känslig information inte loggas, uppmuntrar vi dig att se dessa loggar innan du delar dem.",
|
||||
"preparingLogsTitle": "Förbereder loggar...",
|
||||
"emailLogsTitle": "E-posta loggar",
|
||||
"emailLogsMessage": "Skicka loggarna till {email}",
|
||||
"@emailLogsMessage": {
|
||||
@@ -61,55 +84,111 @@
|
||||
"pleaseWait": "Vänligen vänta...",
|
||||
"generatingEncryptionKeysTitle": "Skapar krypteringsnycklar...",
|
||||
"recreatePassword": "Återskapa lösenord",
|
||||
"recreatePasswordMessage": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).",
|
||||
"useRecoveryKey": "Använd återställningsnyckel",
|
||||
"incorrectPasswordTitle": "Felaktigt lösenord",
|
||||
"welcomeBack": "Välkommen tillbaka!",
|
||||
"madeWithLoveAtPrefix": "gjord med ❤️ av ",
|
||||
"supportDevs": "Prenumerera på <bold-green>ente</bold-green> för att stödja oss",
|
||||
"supportDiscount": "Använd kupongkoden \"AUTH\" för att få 10% rabatt första året",
|
||||
"changeEmail": "Ändra e-postadress",
|
||||
"changePassword": "Ändra lösenord",
|
||||
"data": "Data",
|
||||
"importCodes": "Importera koder",
|
||||
"importTypePlainText": "Enkel text",
|
||||
"importTypeEnteEncrypted": "Ente krypterad export",
|
||||
"passwordForDecryptingExport": "Lösenord för att dekryptera export",
|
||||
"passwordEmptyError": "Lösenordet får inte vara tomt",
|
||||
"importFromApp": "Importera koder från {appName}",
|
||||
"importGoogleAuthGuide": "Exportera dina konton från Google Authenticator till en QR-kod med alternativet \"Överföra konton\". Använd sedan en annan enhet och skanna QR-koden.\n\nTips: Du kan använda din bärbara dators webbkamera för att ta en bild av QR-koden.",
|
||||
"importSelectJsonFile": "Välj JSON-fil",
|
||||
"importSelectAppExport": "Välj {appName} exportfil",
|
||||
"importEnteEncGuide": "Välj den krypterade JSON-filen som exporteras från Ente",
|
||||
"importRaivoGuide": "Använd alternativet \"Exportera OTPs till zip-arkiv\" i Raivos inställningar.\n\nExtrahera zip-filen och importera JSON-filen.",
|
||||
"importBitwardenGuide": "Använd alternativet \"Exportera valv\" inom Bitwarden Tools och importera den okrypterade JSON-filen.",
|
||||
"exportCodes": "Exportera koder",
|
||||
"importLabel": "Importera",
|
||||
"importInstruction": "Vänligen välj en fil som innehåller en lista över dina koder i följande format",
|
||||
"importCodeDelimiterInfo": "Koderna kan separeras med kommatecken eller en ny rad",
|
||||
"selectFile": "Välj fil",
|
||||
"emailVerificationToggle": "E-postverifiering",
|
||||
"emailVerificationEnableWarning": "För att undvika att bli låst från ditt konto, se till att spara en kopia av din e-post 2FA utanför Ente Auth innan du aktiverar e-postverifiering.",
|
||||
"authToChangeEmailVerificationSetting": "Autentisera för att ändra din e-postadress",
|
||||
"authenticateGeneric": "Var god autentisera",
|
||||
"authToViewYourRecoveryKey": "Autentisera för att visa din återställningsnyckel",
|
||||
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
|
||||
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
|
||||
"authToViewSecrets": "Autentisera för att visa din återställningsnyckel",
|
||||
"authToInitiateSignIn": "Vänligen autentisera för att initiera inloggning för säkerhetskopiering.",
|
||||
"ok": "OK",
|
||||
"cancel": "Avbryt",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
"email": "E-post",
|
||||
"support": "Support",
|
||||
"general": "Allmänt",
|
||||
"settings": "Inställningar",
|
||||
"copied": "Kopierat",
|
||||
"pleaseTryAgain": "Försök igen",
|
||||
"existingUser": "Befintlig användare",
|
||||
"newUser": "Ny hos Ente",
|
||||
"delete": "Radera",
|
||||
"enterYourPasswordHint": "Ange ditt lösenord",
|
||||
"forgotPassword": "Glömt lösenord",
|
||||
"oops": "Hoppsan",
|
||||
"suggestFeatures": "Föreslå funktionalitet",
|
||||
"faq": "FAQ",
|
||||
"somethingWentWrongMessage": "Något gick fel, vänligen försök igen",
|
||||
"leaveFamily": "Lämna familjen",
|
||||
"leaveFamilyMessage": "Är du säker på att du vill lämna familjeplanen?",
|
||||
"inFamilyPlanMessage": "Du är på en familjeplan!",
|
||||
"hintForMobile": "Håll i på en kod för att redigera eller ta bort.",
|
||||
"hintForDesktop": "Högerklicka på en kod för att redigera eller ta bort.",
|
||||
"scan": "Skanna",
|
||||
"scanACode": "Skanna kod",
|
||||
"verify": "Verifiera",
|
||||
"verifyEmail": "Verifiera e-postadress",
|
||||
"enterCodeHint": "Ange den 6-siffriga koden från din autentiseringsapp",
|
||||
"lostDeviceTitle": "Förlorad enhet?",
|
||||
"twoFactorAuthTitle": "Tvåfaktorsautentisering",
|
||||
"passkeyAuthTitle": "Lösenordsverifiering",
|
||||
"verifyPasskey": "Verifiera nyckel",
|
||||
"loginWithTOTP": "Logga in med TOTP",
|
||||
"recoverAccount": "Återställ konto",
|
||||
"enterRecoveryKeyHint": "Ange din återställningsnyckel",
|
||||
"recover": "Återställ",
|
||||
"contactSupportViaEmailMessage": "Vänligen skicka ett e-postmeddelande till {email} från din registrerade e-postadress",
|
||||
"@contactSupportViaEmailMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "Ogiltig QR-kod",
|
||||
"noRecoveryKeyTitle": "Ingen återställningsnyckel?",
|
||||
"enterEmailHint": "Ange din e-postadress",
|
||||
"invalidEmailTitle": "Ogiltig e-postadress",
|
||||
"invalidEmailMessage": "Ange en giltig e-postadress.",
|
||||
"deleteAccount": "Radera konto",
|
||||
"deleteAccountQuery": "Vi kommer att vara ledsna över att se dig gå. Har du något problem?",
|
||||
"yesSendFeedbackAction": "Ja, skicka feedback",
|
||||
"noDeleteAccountAction": "Nej, radera konto",
|
||||
"initiateAccountDeleteTitle": "Vänligen autentisera för att initiera borttagning av konto",
|
||||
"sendEmail": "Skicka e-post",
|
||||
"createNewAccount": "Skapa nytt konto",
|
||||
"weakStrength": "Svag",
|
||||
"strongStrength": "Stark",
|
||||
"moderateStrength": "Måttligt",
|
||||
"confirmPassword": "Bekräfta lösenord",
|
||||
"close": "Stäng",
|
||||
"oopsSomethingWentWrong": "Hoppsan! Något gick fel.",
|
||||
"selectLanguage": "Välj språk",
|
||||
"language": "Språk",
|
||||
"social": "Social",
|
||||
"security": "Säkerhet",
|
||||
"lockscreen": "Låsskärm",
|
||||
"authToChangeLockscreenSetting": "Vänligen autentisera för att ändra låsskärms inställningar",
|
||||
"viewActiveSessions": "Visa aktiva sessioner",
|
||||
"authToViewYourActiveSessions": "Autentisera för att visa dina aktiva sessioner",
|
||||
"searchHint": "Sök...",
|
||||
@@ -128,9 +207,13 @@
|
||||
"error": "Fel",
|
||||
"recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
|
||||
"recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
|
||||
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ords nyckel på en säker plats.",
|
||||
"doThisLater": "Gör detta senare",
|
||||
"saveKey": "Spara nyckel",
|
||||
"save": "Spara",
|
||||
"send": "Skicka",
|
||||
"saveOrSendDescription": "Vill du spara detta till din lagringsmapp (Nedladdningsmappen som standard) eller skicka den till andra appar?",
|
||||
"saveOnlyDescription": "Vill du spara detta till din lagringsmapp (Nedladdningsmappen som standard)?",
|
||||
"back": "Tillbaka",
|
||||
"createAccount": "Skapa konto",
|
||||
"passwordStrength": "Lösenordsstyrka: {passwordStrengthValue}",
|
||||
@@ -146,6 +229,7 @@
|
||||
"message": "Password Strength: {passwordStrengthText}"
|
||||
},
|
||||
"password": "Lösenord",
|
||||
"signUpTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
|
||||
"privacyPolicyTitle": "Integritetspolicy",
|
||||
"termsOfServicesTitle": "Villkor",
|
||||
"encryption": "Kryptering",
|
||||
@@ -153,24 +237,58 @@
|
||||
"changePasswordTitle": "Ändra lösenord",
|
||||
"resetPasswordTitle": "Återställ lösenord",
|
||||
"encryptionKeys": "Krypteringsnycklar",
|
||||
"passwordWarning": "Vi lagrar inte detta lösenord, så om du glömmer bort det, <underline>kan vi inte dekryptera dina data</underline>",
|
||||
"enterPasswordToEncrypt": "Ange ett lösenord som vi kan använda för att kryptera din data",
|
||||
"enterNewPasswordToEncrypt": "Ange ett nytt lösenord som vi kan använda för att kryptera din data",
|
||||
"passwordChangedSuccessfully": "Lösenordet har ändrats",
|
||||
"generatingEncryptionKeys": "Skapar krypteringsnycklar...",
|
||||
"continueLabel": "Fortsätt",
|
||||
"insecureDevice": "Osäker enhet",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Tyvärr, kunde vi inte generera säkra nycklar på den här enheten.\n\nvänligen registrera dig från en annan enhet.",
|
||||
"howItWorks": "Så här fungerar det",
|
||||
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>end-to-end-krypterad</underline>.",
|
||||
"loginTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
|
||||
"logInLabel": "Logga in",
|
||||
"logout": "Logga ut",
|
||||
"areYouSureYouWantToLogout": "Är du säker på att du vill logga ut?",
|
||||
"yesLogout": "Ja, logga ut",
|
||||
"exit": "Avsluta",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Ljust",
|
||||
"darkTheme": "Mörkt",
|
||||
"systemTheme": "System",
|
||||
"verifyingRecoveryKey": "Verifierar återställningsnyckel...",
|
||||
"recoveryKeyVerified": "Återställningsnyckel verifierad",
|
||||
"recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.",
|
||||
"recreatePasswordTitle": "Återskapa lösenord",
|
||||
"invalidKey": "Ogiltig nyckel",
|
||||
"tryAgain": "Försök igen",
|
||||
"viewRecoveryKey": "Visa återställningsnyckel",
|
||||
"confirmRecoveryKey": "Bekräfta återställningsnyckel",
|
||||
"confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
|
||||
"confirm": "Bekräfta",
|
||||
"emailYourLogs": "Maila dina loggar",
|
||||
"copyEmailAddress": "Kopiera e-postadress",
|
||||
"exportLogs": "Exportera loggar",
|
||||
"enterYourRecoveryKey": "Ange din återställningsnyckel",
|
||||
"about": "Om",
|
||||
"weAreOpenSource": "Vi är öppen källkod!",
|
||||
"privacy": "Sekretess",
|
||||
"terms": "Villkor",
|
||||
"checkForUpdates": "Sök efter uppdateringar",
|
||||
"checkStatus": "Kontrollera status",
|
||||
"downloadUpdate": "Ladda ner",
|
||||
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
|
||||
"update": "Uppdatera",
|
||||
"checking": "Kontrollerar ...",
|
||||
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
|
||||
"warning": "Varning",
|
||||
"iUnderStand": "Jag förstår",
|
||||
"@iUnderStand": {
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
},
|
||||
"authToExportCodes": "Autentisera för att exportera dina koder",
|
||||
"importSuccessTitle": "Jippi!",
|
||||
"importSuccessDesc": "Du har importerat {count} koder!",
|
||||
"@importSuccessDesc": {
|
||||
"placeholders": {
|
||||
@@ -181,40 +299,108 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sorry": "Tyvärr",
|
||||
"pendingSyncs": "Varning",
|
||||
"activeSessions": "Aktiva sessioner",
|
||||
"incorrectCode": "Felaktig kod",
|
||||
"authenticationSuccessful": "Autentisering lyckades!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd",
|
||||
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
|
||||
"enterPassword": "Ange lösenord",
|
||||
"selectExportFormat": "Välj exportformat",
|
||||
"encrypted": "Krypterad",
|
||||
"plainText": "Enkel text",
|
||||
"passwordToEncryptExport": "Lösenord för att kryptera export",
|
||||
"export": "Exportera",
|
||||
"useOffline": "Använd utan säkerhetskopior",
|
||||
"signInToBackup": "Logga in för att säkerhetskopiera dina koder",
|
||||
"singIn": "Logga in",
|
||||
"showLargeIcons": "Visa stora ikoner",
|
||||
"shouldHideCode": "Dölj koder",
|
||||
"minimizeAppOnCopy": "Minimera appen vid kopiering",
|
||||
"editCodeAuthMessage": "Autentisera för att redigera kod",
|
||||
"deleteCodeAuthMessage": "Autentisera för att radera kod",
|
||||
"showQRAuthMessage": "Autentisera för att visa QR-kod",
|
||||
"confirmAccountDeleteTitle": "Bekräfta radering av kontot",
|
||||
"androidBiometricHint": "Verifiera identitet",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricNotRecognized": "Ej godkänd. Försök igen.",
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Slutförd",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidCancelButton": "Avbryt",
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Obligatorisk autentisering",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "Biometriska uppgifter krävs",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsRequiredTitle": "Enhetsuppgifter krävs",
|
||||
"@androidDeviceCredentialsRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up credentials authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidDeviceCredentialsSetupDescription": "Enhetsuppgifter krävs",
|
||||
"@androidDeviceCredentialsSetupDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure device credentials on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
"goToSettings": "Gå till inställningar",
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"iOSOkButton": "OK",
|
||||
"@iOSOkButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "Ingen internetanslutning",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen.",
|
||||
"signOutFromOtherDevices": "Logga ut från andra enheter",
|
||||
"signOutOtherDevices": "Logga ut andra enheter",
|
||||
"doNotSignOut": "Logga inte ut",
|
||||
"hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)",
|
||||
"hearUsExplanation": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!",
|
||||
"recoveryKeySaved": "Återställningsnyckel sparad i nedladdningsmappen!",
|
||||
"waitingForBrowserRequest": "Väntar på webbläsarbegäran...",
|
||||
"waitingForVerification": "Väntar på verifiering...",
|
||||
"passkey": "Nyckel",
|
||||
"passKeyPendingVerification": "Verifiering pågår fortfarande",
|
||||
"loginSessionExpired": "Sessionen har gått ut",
|
||||
"loginSessionExpiredDetails": "Din session har upphört. Logga in igen.",
|
||||
"developerSettingsWarning": "Är du säker på att du vill ändra på utvecklarinställningar?",
|
||||
"developerSettings": "Utvecklarinställningar",
|
||||
"serverEndpoint": "Serverns slutpunkt",
|
||||
"invalidEndpoint": "Ogiltig slutpunkt",
|
||||
"invalidEndpointMessage": "Tyvärr, slutpunkten du angav är ogiltig. Ange en giltig slutpunkt och försök igen.",
|
||||
"endpointUpdatedMessage": "Slutpunkten har uppdaterats",
|
||||
"customEndpoint": "Ansluten till {endpoint}",
|
||||
"pinText": "Fäst",
|
||||
"unpinText": "Ångra fäst",
|
||||
"pinnedCodeMessage": "{code} har fästs",
|
||||
"pinned": "Fastmarkerad",
|
||||
"tags": "Taggar",
|
||||
"createNewTag": "Skapa ny tagg",
|
||||
"tag": "Tagg",
|
||||
"create": "Skapa",
|
||||
"editTag": "Redigera tagg",
|
||||
"deleteTagTitle": "Radera tagg?",
|
||||
"updateNotAvailable": "Uppdateringen är inte tillgänglig",
|
||||
"viewRawCodes": "Visa råa koder",
|
||||
"rawCodes": "Råa koder",
|
||||
"rawCodeData": "Rå koddata",
|
||||
"appLock": "Applås",
|
||||
"noSystemLockFound": "Inget systemlås hittades",
|
||||
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "För att aktivera applås, vänligen ställ in enhetens lösenord eller skärmlås i systeminställningarna.",
|
||||
"autoLock": "Automatisk låsning",
|
||||
"immediately": "Omedelbart",
|
||||
"reEnterPassword": "Ange lösenord igen",
|
||||
"reEnterPin": "Ange PIN-kod igen",
|
||||
@@ -224,6 +410,8 @@
|
||||
"setNewPassword": "Ställ in nytt lösenord",
|
||||
"deviceLock": "Enhetslås",
|
||||
"hideContent": "Dölj innehåll",
|
||||
"hideContentDescriptionAndroid": "Döljer appinnehåll i app-växlaren och inaktiverar skärmdumpar",
|
||||
"hideContentDescriptioniOS": "Döljer appinnehåll i app-växlaren",
|
||||
"enterPin": "Ange PIN-kod",
|
||||
"setNewPin": "Ställ in ny PIN-kod",
|
||||
"authToViewPasskey": "Autentisera för att visa nyckel"
|
||||
|
||||
@@ -446,8 +446,6 @@
|
||||
"customEndpoint": "Bağlandı: {endpoint}",
|
||||
"pinText": "Sabitle",
|
||||
"unpinText": "Sabitlemeyi kaldır",
|
||||
"pinnedCodeMessage": "{code} sabitlendi",
|
||||
"unpinnedCodeMessage": "{code} sabitlemesi kaldırıldı",
|
||||
"tags": "Etiketler",
|
||||
"createNewTag": "Yeni etiket oluştur",
|
||||
"tag": "Etiket",
|
||||
|
||||
@@ -115,14 +115,14 @@
|
||||
"importCodeDelimiterInfo": "Коди можуть бути розділені комою або новим рядком",
|
||||
"selectFile": "Вибрати файл",
|
||||
"emailVerificationToggle": "Підтвердження адреси електронної пошти",
|
||||
"emailVerificationEnableWarning": "Щоб уникнути блокування доступу до свого облікового запису, обов’язково збережіть копію двофакторної аутентифікації до своєї електронної пошти за межами Ente Auth, перш ніж увімкнути перевірку електронної пошти.",
|
||||
"authToChangeEmailVerificationSetting": "Будь ласка, пройдіть аутентифікацію, щоб змінити перевірку адреси електронної пошти",
|
||||
"emailVerificationEnableWarning": "Щоб уникнути блокування доступу до свого облікового запису, обов’язково збережіть копію двоетапної автентифікації до своєї електронної пошти за межами Ente Auth, перш ніж увімкнути перевірку електронної пошти.",
|
||||
"authToChangeEmailVerificationSetting": "Будь ласка, пройдіть автентифікацію, щоб змінити перевірку адреси електронної пошти",
|
||||
"authenticateGeneric": "Будь ласка, авторизуйтеся",
|
||||
"authToViewYourRecoveryKey": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваш ключ відновлення",
|
||||
"authToChangeYourEmail": "Будь ласка, пройдіть аутентифікацію, щоб змінити адресу електронної пошти",
|
||||
"authToChangeYourPassword": "Будь ласка, пройдіть аутентифікацію, щоб змінити ваш пароль",
|
||||
"authToViewSecrets": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваші секретні коди",
|
||||
"authToInitiateSignIn": "Будь ласка, пройдіть аутентифікацію, щоб розпочати вхід для резервного копіювання.",
|
||||
"authToViewYourRecoveryKey": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваш ключ відновлення",
|
||||
"authToChangeYourEmail": "Будь ласка, пройдіть автентифікацію, щоб змінити адресу електронної пошти",
|
||||
"authToChangeYourPassword": "Будь ласка, пройдіть автентифікацію, щоб змінити ваш пароль",
|
||||
"authToViewSecrets": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваші секретні коди",
|
||||
"authToInitiateSignIn": "Будь ласка, пройдіть автентифікацію, щоб розпочати вхід для резервного копіювання.",
|
||||
"ok": "Ок",
|
||||
"cancel": "Скасувати",
|
||||
"yes": "Так",
|
||||
@@ -153,7 +153,7 @@
|
||||
"verifyEmail": "Підтвердити електронну адресу",
|
||||
"enterCodeHint": "Введіть нижче шестизначний код із застосунку для автентифікації",
|
||||
"lostDeviceTitle": "Загубили пристрій?",
|
||||
"twoFactorAuthTitle": "Двофакторна аутентифікація",
|
||||
"twoFactorAuthTitle": "Двоетапна автентифікація",
|
||||
"passkeyAuthTitle": "Перевірка секретного ключа",
|
||||
"verifyPasskey": "Підтвердження секретного ключа",
|
||||
"loginWithTOTP": "Увійти за допомогою TOTP",
|
||||
@@ -194,7 +194,7 @@
|
||||
"authToChangeLockscreenSetting": "Будь ласка, авторизуйтесь для зміни налаштувань екрану блокування",
|
||||
"deviceLockEnablePreSteps": "Для увімкнення блокування програми, будь ласка, налаштуйте пароль пристрою або блокування екрана в системних налаштуваннях.",
|
||||
"viewActiveSessions": "Показати активні сеанси",
|
||||
"authToViewYourActiveSessions": "Будь ласка, пройдіть аутентифікацію, щоб переглянути ваші активні сеанси",
|
||||
"authToViewYourActiveSessions": "Будь ласка, пройдіть автентифікацію, щоб переглянути ваші активні сеанси",
|
||||
"searchHint": "Пошук...",
|
||||
"search": "Пошук",
|
||||
"sorryUnableToGenCode": "Вибачте, не вдалося створити код для {issuerName}",
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Ви впевнені, що хочете вийти з системи?",
|
||||
"yesLogout": "Так, вийти з системи",
|
||||
"exit": "Вийти",
|
||||
"theme": "Тема",
|
||||
"lightTheme": "Світла",
|
||||
"darkTheme": "Темна",
|
||||
"systemTheme": "Як в системі",
|
||||
"verifyingRecoveryKey": "Перевірка ключа відновлення...",
|
||||
"recoveryKeyVerified": "Ключ відновлення перевірено",
|
||||
"recoveryKeySuccessBody": "Чудово! Ваш ключ відновлення дійсний. Дякуємо за перевірку.\n\nБудь ласка, не забувайте зберігати надійну резервну копію ключа відновлення.",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Власні",
|
||||
"editOrder": "Змінити порядок",
|
||||
"mostFrequentlyUsed": "Часто використовувані",
|
||||
"mostRecentlyUsed": "Нещодавно використані",
|
||||
"activeSessions": "Активні сеанси",
|
||||
"somethingWentWrongPleaseTryAgain": "Щось пішло не так, спробуйте, будь ласка, знову",
|
||||
"thisWillLogYouOutOfThisDevice": "Це призведе до виходу на цьому пристрої!",
|
||||
@@ -342,9 +350,9 @@
|
||||
"incorrectCode": "Невірний код",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Вибачте, але введений вами код є невірним",
|
||||
"emailChangedTo": "Адресу електронної пошти змінено на {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "Аутентифікація не пройдена. Будь ласка, спробуйте ще раз",
|
||||
"authenticationFailedPleaseTryAgain": "Автентифікація не пройдена. Будь ласка, спробуйте ще раз",
|
||||
"authenticationSuccessful": "Автентифікацію виконано!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Двофакторна аутентифікація успішно скинута",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Двоетапна автентифікація успішно скинута",
|
||||
"incorrectRecoveryKey": "Неправильний ключ відновлення",
|
||||
"theRecoveryKeyYouEnteredIsIncorrect": "Ви ввели неправильний ключ відновлення",
|
||||
"enterPassword": "Введіть пароль",
|
||||
@@ -366,9 +374,9 @@
|
||||
"focusOnSearchBar": "Сфокусуватися на пошуку після запуску програми",
|
||||
"confirmUpdatingkey": "Ви впевнені у тому, що бажаєте змінити секретний ключ?",
|
||||
"minimizeAppOnCopy": "Згорнути програму після копіювання",
|
||||
"editCodeAuthMessage": "Аутентифікуйтесь, щоб змінити код",
|
||||
"deleteCodeAuthMessage": "Аутентифікуйтесь, щоб видалити код",
|
||||
"showQRAuthMessage": "Аутентифікуйтесь, щоб показати QR-код",
|
||||
"editCodeAuthMessage": "Авторизуйтесь, щоб змінити код",
|
||||
"deleteCodeAuthMessage": "Авторизуйтесь, щоб видалити код",
|
||||
"showQRAuthMessage": "Авторизуйтесь, щоб показати QR-код",
|
||||
"confirmAccountDeleteTitle": "Підтвердіть видалення облікового запису",
|
||||
"confirmAccountDeleteMessage": "Цей обліковий запис є зв'язаним з іншими програмами Ente, якщо ви використовуєте якісь з них.\n\nВаші завантажені дані у всіх програмах Ente будуть заплановані до видалення, а обліковий запис буде видалено назавжди.",
|
||||
"androidBiometricHint": "Підтвердити ідентифікацію",
|
||||
@@ -387,11 +395,11 @@
|
||||
"@androidCancelButton": {
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters."
|
||||
},
|
||||
"androidSignInTitle": "Необхідна аутентифікація",
|
||||
"androidSignInTitle": "Необхідна автентифікація",
|
||||
"@androidSignInTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricRequiredTitle": "Потрібна біометрична аутентифікація",
|
||||
"androidBiometricRequiredTitle": "Потрібна біометрична автентифікація",
|
||||
"@androidBiometricRequiredTitle": {
|
||||
"description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
@@ -407,7 +415,7 @@
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Біометрична аутентифікація не налаштована на вашому пристрої. Перейдіть в 'Налаштування > Безпека', щоб додати біометричну аутентифікацію.",
|
||||
"androidGoToSettingsDescription": "Біометрична автентифікація не налаштована на вашому пристрої. Перейдіть в «Налаштування > Безпека», щоб додати біометричну автентифікацію.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
@@ -415,7 +423,7 @@
|
||||
"@iOSLockOut": {
|
||||
"description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
"iOSGoToSettingsDescription": "Біометрична аутентифікація не налаштована на вашому пристрої. Увімкніть TouchID або FaceID на вашому телефоні.",
|
||||
"iOSGoToSettingsDescription": "Біометрична автентифікація не налаштована на вашому пристрої. Увімкніть TouchID або FaceID на вашому телефоні.",
|
||||
"@iOSGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side."
|
||||
},
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Відкріпити",
|
||||
"pinnedCodeMessage": "{code} закріплено",
|
||||
"unpinnedCodeMessage": "{code} відкріплено",
|
||||
"pinned": "Закріплено",
|
||||
"tags": "Мітки",
|
||||
"createNewTag": "Створити нову мітку",
|
||||
"tag": "Мітка",
|
||||
@@ -485,5 +494,13 @@
|
||||
"appLockNotEnabled": "Блокування програм не увімкнено",
|
||||
"appLockNotEnabledDescription": "Увімкніть блокування програм від безпеки > Блокування програм",
|
||||
"authToViewPasskey": "Будь ласка, авторизуйтеся для перегляду ключа доступу",
|
||||
"appLockOfflineModeWarning": "Ви обрали продовжити без резервних копій. Якщо ви забудете свій пароль, доступ до ваших даних буде заблоковано."
|
||||
"appLockOfflineModeWarning": "Ви обрали продовжити без резервних копій. Якщо ви забудете свій пароль, доступ до ваших даних буде заблоковано.",
|
||||
"duplicateCodes": "Дублювати коди",
|
||||
"noDuplicates": "✨ Немає дублікатів",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "У вас немає дублікатів кодів, які можна очистити",
|
||||
"deduplicateCodes": "Дедуплікувати коди",
|
||||
"deselectAll": "Зняти виділення",
|
||||
"selectAll": "Вибрати все",
|
||||
"deleteDuplicates": "Видалити дублікати",
|
||||
"plainHTML": "Звичайний HTML"
|
||||
}
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "Bạn có chắc chắn muốn đăng xuất?",
|
||||
"yesLogout": "Có, đăng xuất",
|
||||
"exit": "Thoát",
|
||||
"theme": "Chủ đề",
|
||||
"lightTheme": "Sáng",
|
||||
"darkTheme": "Tối",
|
||||
"systemTheme": "Hệ thống",
|
||||
"verifyingRecoveryKey": "Đang xác minh khóa khôi phục...",
|
||||
"recoveryKeyVerified": "Khóa khôi phục đã được xác thực",
|
||||
"recoveryKeySuccessBody": "Tuyệt vời! Khóa khôi phục của bạn hợp lệ. Cảm ơn bạn đã xác minh.\n\nHãy nhớ sao lưu khóa khôi phục của bạn một cách an toàn.",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Tùy chỉnh",
|
||||
"editOrder": "Chỉnh sửa đơn hàng",
|
||||
"mostFrequentlyUsed": "Thường dùng",
|
||||
"mostRecentlyUsed": "Dùng gần đây",
|
||||
"activeSessions": "Các phiên làm việc hiện tại",
|
||||
"somethingWentWrongPleaseTryAgain": "Phát hiện có lỗi, xin thử lại",
|
||||
"thisWillLogYouOutOfThisDevice": "Thao tác này sẽ đăng xuất bạn khỏi thiết bị này!",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "Bỏ ghim",
|
||||
"pinnedCodeMessage": "{code} đã được ghim",
|
||||
"unpinnedCodeMessage": "{code} đã được bỏ ghim",
|
||||
"pinned": "Đã ghim",
|
||||
"tags": "Thẻ",
|
||||
"createNewTag": "Tạo thẻ mới",
|
||||
"tag": "Thẻ",
|
||||
@@ -485,5 +494,12 @@
|
||||
"appLockNotEnabled": "Khóa ứng dụng chưa được bật",
|
||||
"appLockNotEnabledDescription": "Vui lòng bật khóa ứng dụng từ Bảo mật > Khóa ứng dụng",
|
||||
"authToViewPasskey": "Vui lòng xác thực để xem mã khóa",
|
||||
"appLockOfflineModeWarning": "Bạn đã chọn tiếp tục mà không có bản sao lưu. Nếu bạn quên khóa ứng dụng, bạn sẽ bị khóa khỏi việc truy cập dữ liệu của mình."
|
||||
"appLockOfflineModeWarning": "Bạn đã chọn tiếp tục mà không có bản sao lưu. Nếu bạn quên khóa ứng dụng, bạn sẽ bị khóa khỏi việc truy cập dữ liệu của mình.",
|
||||
"duplicateCodes": "Mã trùng lặp",
|
||||
"noDuplicates": "✨ Không có trùng lặp",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Bạn không có mã trùng lặp nào có thể được xóa",
|
||||
"deduplicateCodes": "Loại bỏ mã trùng lặp",
|
||||
"deselectAll": "Bỏ chọn tất cả",
|
||||
"selectAll": "Chọn tất cả",
|
||||
"deleteDuplicates": "Xóa trùng lặp"
|
||||
}
|
||||
@@ -258,6 +258,10 @@
|
||||
"areYouSureYouWantToLogout": "您确定要登出吗?",
|
||||
"yesLogout": "是的,登出",
|
||||
"exit": "退出",
|
||||
"theme": "主题",
|
||||
"lightTheme": "浅色",
|
||||
"darkTheme": "深色",
|
||||
"systemTheme": "系统",
|
||||
"verifyingRecoveryKey": "正在验证恢复密钥...",
|
||||
"recoveryKeyVerified": "恢复密钥已验证",
|
||||
"recoveryKeySuccessBody": "太棒了! 您的恢复密钥是有效的。 感谢您的验证。\n\n请记住要安全备份您的恢复密钥。",
|
||||
@@ -328,6 +332,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "自定义",
|
||||
"editOrder": "编辑顺序",
|
||||
"mostFrequentlyUsed": "经常使用",
|
||||
"mostRecentlyUsed": "最近使用",
|
||||
"activeSessions": "已登录的设备",
|
||||
"somethingWentWrongPleaseTryAgain": "出了点问题,请重试",
|
||||
"thisWillLogYouOutOfThisDevice": "这将使您登出该设备!",
|
||||
@@ -449,6 +457,7 @@
|
||||
"unpinText": "取消置顶",
|
||||
"pinnedCodeMessage": "{code} 已被置顶",
|
||||
"unpinnedCodeMessage": "{code} 已被取消置顶",
|
||||
"pinned": "已置顶",
|
||||
"tags": "标签",
|
||||
"createNewTag": "创建新标签",
|
||||
"tag": "标签",
|
||||
@@ -485,5 +494,13 @@
|
||||
"appLockNotEnabled": "应用锁未启用",
|
||||
"appLockNotEnabledDescription": "请从“安全”>“应用锁”启用应用锁",
|
||||
"authToViewPasskey": "请验证身份以查看通行密钥",
|
||||
"appLockOfflineModeWarning": "您已选择继续而不备份。如果您忘记了应用锁,您将无法访问数据。"
|
||||
"appLockOfflineModeWarning": "您已选择继续而不备份。如果您忘记了应用锁,您将无法访问数据。",
|
||||
"duplicateCodes": "重复代码",
|
||||
"noDuplicates": "✨ 没有重复",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "您没有可清除的重复代码",
|
||||
"deduplicateCodes": "删除重复代码",
|
||||
"deselectAll": "取消全选",
|
||||
"selectAll": "全选",
|
||||
"deleteDuplicates": "删除重复项",
|
||||
"plainHTML": "Plain HTML"
|
||||
}
|
||||
15
auth/lib/models/all_icon_data.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
enum IconType { simpleIcon, customIcon }
|
||||
|
||||
class AllIconData {
|
||||
final String title;
|
||||
final IconType type;
|
||||
final String? color;
|
||||
final String? slug;
|
||||
|
||||
AllIconData({
|
||||
required this.title,
|
||||
required this.type,
|
||||
required this.color,
|
||||
this.slug,
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,8 @@ class CodeDisplay {
|
||||
String note;
|
||||
final List<String> tags;
|
||||
int position;
|
||||
String iconSrc;
|
||||
String iconID;
|
||||
|
||||
CodeDisplay({
|
||||
this.pinned = false,
|
||||
@@ -21,8 +23,12 @@ class CodeDisplay {
|
||||
this.tags = const [],
|
||||
this.note = '',
|
||||
this.position = 0,
|
||||
this.iconSrc = '',
|
||||
this.iconID = '',
|
||||
});
|
||||
|
||||
bool get isCustomIcon => (iconSrc != '' && iconID != '');
|
||||
|
||||
// copyWith
|
||||
CodeDisplay copyWith({
|
||||
bool? pinned,
|
||||
@@ -32,6 +38,8 @@ class CodeDisplay {
|
||||
List<String>? tags,
|
||||
String? note,
|
||||
int? position,
|
||||
String? iconSrc,
|
||||
String? iconID,
|
||||
}) {
|
||||
final bool updatedPinned = pinned ?? this.pinned;
|
||||
final bool updatedTrashed = trashed ?? this.trashed;
|
||||
@@ -40,6 +48,8 @@ class CodeDisplay {
|
||||
final List<String> updatedTags = tags ?? this.tags;
|
||||
final String updatedNote = note ?? this.note;
|
||||
final int updatedPosition = position ?? this.position;
|
||||
final String updatedIconSrc = iconSrc ?? this.iconSrc;
|
||||
final String updatedIconID = iconID ?? this.iconID;
|
||||
|
||||
return CodeDisplay(
|
||||
pinned: updatedPinned,
|
||||
@@ -49,6 +59,8 @@ class CodeDisplay {
|
||||
tags: updatedTags,
|
||||
note: updatedNote,
|
||||
position: updatedPosition,
|
||||
iconSrc: updatedIconSrc,
|
||||
iconID: updatedIconID,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,6 +76,8 @@ class CodeDisplay {
|
||||
tags: List<String>.from(json['tags'] ?? []),
|
||||
note: json['note'] ?? '',
|
||||
position: json['position'] ?? 0,
|
||||
iconSrc: json['iconSrc'] ?? 'ente',
|
||||
iconID: json['iconID'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +120,8 @@ class CodeDisplay {
|
||||
'tags': tags,
|
||||
'note': note,
|
||||
'position': position,
|
||||
'iconSrc': iconSrc,
|
||||
'iconID': iconID,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/all_icon_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/code_display.dart';
|
||||
import 'package:ente_auth/onboarding/model/tag_enums.dart';
|
||||
@@ -13,7 +14,10 @@ import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/custom_icon_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/ui/custom_icon_page.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
@@ -42,6 +46,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
late List<String> selectedTags = [...?widget.code?.display.tags];
|
||||
List<String> allTags = [];
|
||||
StreamSubscription<CodesUpdatedEvent>? _streamSubscription;
|
||||
bool isCustomIcon = false;
|
||||
String _customIconID = "";
|
||||
late IconType _iconSrc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -81,6 +88,19 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
_limitTextLength(_accountController, _otherTextLimit);
|
||||
_limitTextLength(_secretController, _otherTextLimit);
|
||||
}
|
||||
|
||||
isCustomIcon = widget.code?.display.isCustomIcon ?? false;
|
||||
if (isCustomIcon) {
|
||||
_customIconID = widget.code?.display.iconID ?? "ente";
|
||||
} else {
|
||||
if (widget.code != null) {
|
||||
_customIconID = widget.code!.issuer;
|
||||
}
|
||||
}
|
||||
_iconSrc = widget.code?.display.iconSrc == "simpleIcon"
|
||||
? IconType.simpleIcon
|
||||
: IconType.customIcon;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -120,191 +140,208 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
if (widget.code != null)
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await navigateToCustomIconPage();
|
||||
},
|
||||
child: CustomIconWidget(iconData: _customIconID),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FieldLabel(l10n.codeIssuerHint),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.secret),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: getEnteTextTheme(context).small,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0),
|
||||
suffixIcon: GestureDetector(
|
||||
// padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_secretKeyObscured = !_secretKeyObscured;
|
||||
});
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.codeIssuerHint),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: _secretKeyObscured
|
||||
? const Icon(
|
||||
Icons.visibility_off_rounded,
|
||||
size: 18,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.visibility_rounded,
|
||||
size: 18,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
obscureText: _secretKeyObscured,
|
||||
controller: _secretController,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.account),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.secret),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: getEnteTextTheme(context).small,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 12.0),
|
||||
suffixIcon: GestureDetector(
|
||||
// padding: EdgeInsets.zero,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_secretKeyObscured = !_secretKeyObscured;
|
||||
});
|
||||
},
|
||||
child: _secretKeyObscured
|
||||
? const Icon(
|
||||
Icons.visibility_off_rounded,
|
||||
size: 18,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.visibility_rounded,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
obscureText: _secretKeyObscured,
|
||||
controller: _secretController,
|
||||
),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _accountController,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.notes),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
if (value.length > _notesLimit) {
|
||||
return "Notes can't be more than 1000 characters";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
maxLength: _notesLimit,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.account),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _accountController,
|
||||
),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _notesController,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
alignment: WrapAlignment.start,
|
||||
children: [
|
||||
...allTags.map(
|
||||
(e) => TagChip(
|
||||
label: e,
|
||||
action: TagChipAction.check,
|
||||
state: selectedTags.contains(e)
|
||||
? TagChipState.selected
|
||||
: TagChipState.unselected,
|
||||
onTap: () {
|
||||
if (selectedTags.contains(e)) {
|
||||
selectedTags.remove(e);
|
||||
} else {
|
||||
selectedTags.add(e);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
FieldLabel(l10n.notes),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
if (value.length > _notesLimit) {
|
||||
return "Notes can't be more than 1000 characters";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
maxLength: _notesLimit,
|
||||
minLines: 1,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(vertical: 12.0),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _notesController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AddChip(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AddTagDialog(
|
||||
onTap: (tag) {
|
||||
final exist = allTags.contains(tag);
|
||||
if (exist && selectedTags.contains(tag)) {
|
||||
return Navigator.pop(context);
|
||||
}
|
||||
if (!exist) allTags.add(tag);
|
||||
selectedTags.add(tag);
|
||||
setState(() {});
|
||||
Navigator.pop(context);
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
alignment: WrapAlignment.start,
|
||||
children: [
|
||||
...allTags.map(
|
||||
(e) => TagChip(
|
||||
label: e,
|
||||
action: TagChipAction.check,
|
||||
state: selectedTags.contains(e)
|
||||
? TagChipState.selected
|
||||
: TagChipState.unselected,
|
||||
onTap: () {
|
||||
if (selectedTags.contains(e)) {
|
||||
selectedTags.remove(e);
|
||||
} else {
|
||||
selectedTags.add(e);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
AddChip(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AddTagDialog(
|
||||
onTap: (tag) {
|
||||
final exist = allTags.contains(tag);
|
||||
if (exist && selectedTags.contains(tag)) {
|
||||
return Navigator.pop(context);
|
||||
}
|
||||
if (!exist) allTags.add(tag);
|
||||
selectedTags.add(tag);
|
||||
setState(() {});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
if ((_accountController.text.trim().isEmpty &&
|
||||
_issuerController.text.trim().isEmpty) ||
|
||||
_secretController.text.trim().isEmpty) {
|
||||
String message;
|
||||
if (_secretController.text.trim().isEmpty) {
|
||||
message = context.l10n.secretCanNotBeEmpty;
|
||||
} else {
|
||||
message =
|
||||
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
|
||||
}
|
||||
_showIncorrectDetailsDialog(
|
||||
context,
|
||||
message: message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _saveCode();
|
||||
},
|
||||
child: Text(l10n.saveAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
if ((_accountController.text.trim().isEmpty &&
|
||||
_issuerController.text.trim().isEmpty) ||
|
||||
_secretController.text.trim().isEmpty) {
|
||||
String message;
|
||||
if (_secretController.text.trim().isEmpty) {
|
||||
message = context.l10n.secretCanNotBeEmpty;
|
||||
} else {
|
||||
message =
|
||||
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
|
||||
}
|
||||
_showIncorrectDetailsDialog(context, message: message);
|
||||
return;
|
||||
}
|
||||
await _saveCode();
|
||||
},
|
||||
child: Text(l10n.saveAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -324,6 +361,11 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
widget.code?.display.copyWith(tags: selectedTags) ??
|
||||
CodeDisplay(tags: selectedTags);
|
||||
display.note = notes;
|
||||
|
||||
display.iconID = _customIconID.toLowerCase();
|
||||
display.iconSrc =
|
||||
_iconSrc == IconType.simpleIcon ? 'simpleIcon' : 'customIcon';
|
||||
|
||||
if (widget.code != null && widget.code!.secret != secret) {
|
||||
ButtonResult? result = await showChoiceActionSheet(
|
||||
context,
|
||||
@@ -373,4 +415,28 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
message ?? context.l10n.pleaseVerifyDetails,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> navigateToCustomIconPage() async {
|
||||
final allIcons = IconUtils.instance.getAllIcons();
|
||||
String currentIcon;
|
||||
if (widget.code!.display.isCustomIcon) {
|
||||
currentIcon = widget.code!.display.iconID;
|
||||
} else {
|
||||
currentIcon = widget.code!.issuer;
|
||||
}
|
||||
final AllIconData newCustomIcon = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return CustomIconPage(
|
||||
currentIcon: currentIcon,
|
||||
allIcons: allIcons,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_customIconID = newCustomIcon.title;
|
||||
_iconSrc = newCustomIcon.type;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
56
auth/lib/services/deduplication_service.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class DeduplicationService {
|
||||
final _logger = Logger("DeduplicationService");
|
||||
|
||||
DeduplicationService._privateConstructor();
|
||||
|
||||
static final DeduplicationService instance =
|
||||
DeduplicationService._privateConstructor();
|
||||
|
||||
Future<List<DuplicateCodes>> getDuplicateCodes() async {
|
||||
try {
|
||||
final List<DuplicateCodes> result = await _getDuplicateCodes();
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to get dedupeCode", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<DuplicateCodes>> _getDuplicateCodes() async {
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
final List<DuplicateCodes> duplicateCodes = [];
|
||||
Map<String, List<Code>> uniqueCodes = {};
|
||||
|
||||
for (final code in codes) {
|
||||
if (code.hasError || code.isTrashed) continue;
|
||||
|
||||
final uniqueKey = "${code.secret}_${code.issuer}_${code.account}";
|
||||
|
||||
if (uniqueCodes.containsKey(uniqueKey)) {
|
||||
uniqueCodes[uniqueKey]!.add(code);
|
||||
} else {
|
||||
uniqueCodes[uniqueKey] = [code];
|
||||
}
|
||||
}
|
||||
for (final key in uniqueCodes.keys) {
|
||||
if (uniqueCodes[key]!.length > 1) {
|
||||
duplicateCodes.add(DuplicateCodes(key, uniqueCodes[key]!));
|
||||
}
|
||||
}
|
||||
return duplicateCodes;
|
||||
}
|
||||
}
|
||||
|
||||
class DuplicateCodes {
|
||||
String hash;
|
||||
final List<Code> codes;
|
||||
|
||||
DuplicateCodes(
|
||||
this.hash,
|
||||
this.codes,
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@@ -11,11 +11,13 @@ class PasskeyService {
|
||||
|
||||
final _enteDio = Network.instance.enteDio;
|
||||
|
||||
Future<String> getJwtToken() async {
|
||||
Future<String> getAccountsUrl() async {
|
||||
final response = await _enteDio.get(
|
||||
"/users/accounts-token",
|
||||
);
|
||||
return response.data!["accountsToken"] as String;
|
||||
final accountsUrl = response.data!["accountsUrl"] ?? kAccountsUrl;
|
||||
final jwtToken = response.data!["accountsToken"] as String;
|
||||
return "$accountsUrl/passkeys?token=$jwtToken";
|
||||
}
|
||||
|
||||
Future<bool> isPasskeyRecoveryEnabled() async {
|
||||
@@ -25,10 +27,6 @@ class PasskeyService {
|
||||
return response.data!["isPasskeyRecoveryEnabled"] as bool;
|
||||
}
|
||||
|
||||
String get accountsUrl {
|
||||
return kDebugMode ? "http://localhost:3001" : "https://accounts.ente.io";
|
||||
}
|
||||
|
||||
Future<void> configurePasskeyRecovery(
|
||||
String secret,
|
||||
String userEncryptedSecret,
|
||||
@@ -46,8 +44,7 @@ class PasskeyService {
|
||||
|
||||
Future<void> openPasskeyPage(BuildContext context) async {
|
||||
try {
|
||||
final jwtToken = await getJwtToken();
|
||||
final url = "$accountsUrl/passkeys?token=$jwtToken";
|
||||
final url = await getAccountsUrl();
|
||||
await launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
|
||||
@@ -379,6 +379,7 @@ class UserService {
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl;
|
||||
String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID.isEmpty &&
|
||||
response.data["twoFactorSessionIDV2"] != null) {
|
||||
@@ -388,6 +389,7 @@ class UserService {
|
||||
page = PasskeyPage(
|
||||
passkeySessionID,
|
||||
totp2FASessionID: twoFASessionID,
|
||||
accountsUrl: accountsUrl,
|
||||
);
|
||||
} else if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
@@ -692,6 +694,7 @@ class UserService {
|
||||
if (response.statusCode == 200) {
|
||||
Widget? page;
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl;
|
||||
String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID.isEmpty &&
|
||||
response.data["twoFactorSessionIDV2"] != null) {
|
||||
@@ -702,6 +705,7 @@ class UserService {
|
||||
page = PasskeyPage(
|
||||
passkeySessionID,
|
||||
totp2FASessionID: twoFASessionID,
|
||||
accountsUrl: accountsUrl,
|
||||
);
|
||||
} else if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
class CodeTimerProgressCache {
|
||||
static final Map<int, CodeTimerProgress> _cache = {};
|
||||
|
||||
static CodeTimerProgress getCachedWidget(int period) {
|
||||
if (!_cache.containsKey(period)) {
|
||||
_cache[period] = CodeTimerProgress(period: period);
|
||||
}
|
||||
return _cache[period]!;
|
||||
}
|
||||
}
|
||||
|
||||
class CodeTimerProgress extends StatefulWidget {
|
||||
final int period;
|
||||
|
||||
final bool isCompactMode;
|
||||
const CodeTimerProgress({
|
||||
super.key,
|
||||
required this.period,
|
||||
this.isCompactMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CodeTimerProgress> createState() => _CodeTimerProgressState();
|
||||
}
|
||||
|
||||
class _CodeTimerProgressState extends State<CodeTimerProgress>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final Ticker _ticker;
|
||||
class _CodeTimerProgressState extends State<CodeTimerProgress> {
|
||||
late final Timer _timer;
|
||||
late final ValueNotifier<double> _progress;
|
||||
late final int _microSecondsInPeriod;
|
||||
late bool _isCompactMode=false;
|
||||
late final int _periodInMicros;
|
||||
|
||||
// Cache the start time to avoid repeated system calls
|
||||
late final int _startMicros;
|
||||
|
||||
// Reduce update frequency
|
||||
final int _updateIntervalMs =
|
||||
(Platform.isAndroid || Platform.isIOS) ? 16 : 500; // approximately 60 FPS
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_microSecondsInPeriod = widget.period * 1000000;
|
||||
_periodInMicros = widget.period * 1000000;
|
||||
_progress = ValueNotifier<double>(0.0);
|
||||
_ticker = createTicker(_updateTimeRemaining);
|
||||
_ticker.start();
|
||||
_isCompactMode = PreferenceService.instance.isCompactMode();
|
||||
_updateTimeRemaining(Duration.zero);
|
||||
_startMicros = DateTime.now().microsecondsSinceEpoch;
|
||||
|
||||
// Use a Timer instead of a Ticker
|
||||
_timer = Timer.periodic(Duration(milliseconds: _updateIntervalMs), (timer) {
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
_updateTimeRemaining(now);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateTimeRemaining(Duration elapsed) {
|
||||
int timeRemaining = _microSecondsInPeriod -
|
||||
(DateTime.now().microsecondsSinceEpoch % _microSecondsInPeriod);
|
||||
_progress.value = timeRemaining / _microSecondsInPeriod;
|
||||
void _updateTimeRemaining(int currentMicros) {
|
||||
// More efficient time calculation using modulo
|
||||
final elapsed = (currentMicros - _startMicros) % _periodInMicros;
|
||||
final timeRemaining = _periodInMicros - elapsed;
|
||||
_progress.value = timeRemaining / _periodInMicros;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ticker.dispose();
|
||||
_timer.cancel();
|
||||
_progress.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -60,18 +60,19 @@ class _CodeTimerProgressState extends State<CodeTimerProgress>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: _isCompactMode ?1:3,
|
||||
height: widget.isCompactMode ? 1 : 3,
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _progress,
|
||||
builder: (context, progress, _) {
|
||||
return CustomPaint(
|
||||
key: Key(progress.toString()), // Add key here
|
||||
painter: _ProgressPainter(
|
||||
progress: progress,
|
||||
color: progress > 0.4
|
||||
? getEnteColorScheme(context).primary700
|
||||
: Colors.orange,
|
||||
),
|
||||
size: Size.infinite,
|
||||
size: const Size.fromHeight(double.infinity),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -83,7 +84,10 @@ class _ProgressPainter extends CustomPainter {
|
||||
final double progress;
|
||||
final Color color;
|
||||
|
||||
_ProgressPainter({required this.progress, required this.color});
|
||||
const _ProgressPainter({
|
||||
required this.progress,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
|
||||
@@ -146,8 +146,10 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgressCache.getCachedWidget(
|
||||
widget.code.period,
|
||||
CodeTimerProgress(
|
||||
key: ValueKey('period_${widget.code.period}'),
|
||||
period: widget.code.period,
|
||||
isCompactMode: widget.isCompactMode,
|
||||
),
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
@@ -445,13 +447,19 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
}
|
||||
|
||||
Widget _getIcon() {
|
||||
final String iconData;
|
||||
if (widget.code.display.isCustomIcon) {
|
||||
iconData = widget.code.display.iconID;
|
||||
} else {
|
||||
iconData = widget.code.issuer;
|
||||
}
|
||||
return Padding(
|
||||
padding: _shouldShowLargeIcon
|
||||
? EdgeInsets.only(left: widget.isCompactMode ? 12 : 16)
|
||||
: const EdgeInsets.all(0),
|
||||
child: IconUtils.instance.getIcon(
|
||||
context,
|
||||
safeDecode(widget.code.issuer).trim(),
|
||||
safeDecode(iconData).trim(),
|
||||
width: widget.isCompactMode
|
||||
? (_shouldShowLargeIcon ? 32 : 24)
|
||||
: (_shouldShowLargeIcon ? 42 : 24),
|
||||
|
||||
111
auth/lib/ui/components/custom_icon_widget.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:figma_squircle/figma_squircle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomIconWidget extends StatelessWidget {
|
||||
final String iconData;
|
||||
|
||||
CustomIconWidget({
|
||||
super.key,
|
||||
required this.iconData,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 90,
|
||||
width: 90,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: ShapeDecoration(
|
||||
shape: SmoothRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 1.5,
|
||||
color: getEnteColorScheme(context)
|
||||
.tagChipSelectedColor
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
borderRadius: SmoothBorderRadius(
|
||||
cornerRadius: 15.5,
|
||||
cornerSmoothing: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: IconUtils.instance.getIcon(
|
||||
context,
|
||||
safeDecode(iconData).trim(),
|
||||
width: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
_getEditIcon(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getEditIcon(BuildContext context) {
|
||||
return Positioned(
|
||||
left: 60,
|
||||
top: 60,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 28,
|
||||
width: 28,
|
||||
decoration: ShapeDecoration(
|
||||
color: Colors.white,
|
||||
shadows: const [
|
||||
BoxShadow(
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 0.84,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.11),
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(0.84, 0.84),
|
||||
blurRadius: 1.68,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.09),
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(2.53, 2.53),
|
||||
blurRadius: 2.53,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.05),
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(5.05, 4.21),
|
||||
blurRadius: 2.53,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.02),
|
||||
),
|
||||
BoxShadow(
|
||||
offset: Offset(7.58, 6.74),
|
||||
blurRadius: 2.53,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.0),
|
||||
),
|
||||
],
|
||||
shape: SmoothRectangleBorder(
|
||||
borderRadius: SmoothBorderRadius(
|
||||
cornerRadius: 8,
|
||||
cornerSmoothing: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: 16,
|
||||
color: Colors.black.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
220
auth/lib/ui/custom_icon_page.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/all_icon_data.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomIconPage extends StatefulWidget {
|
||||
final Map<String, AllIconData> allIcons;
|
||||
final String currentIcon;
|
||||
|
||||
const CustomIconPage({
|
||||
super.key,
|
||||
required this.allIcons,
|
||||
required this.currentIcon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomIconPage> createState() => _CustomIconPageState();
|
||||
}
|
||||
|
||||
class _CustomIconPageState extends State<CustomIconPage> {
|
||||
Map<String, AllIconData> _filteredIcons = {};
|
||||
bool _showSearchBox = false;
|
||||
final bool _autoFocusSearch =
|
||||
PreferenceService.instance.shouldAutoFocusOnSearchBar();
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
String _searchText = "";
|
||||
|
||||
// Used to request focus on the search box when clicked the search icon
|
||||
late FocusNode searchBoxFocusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_filteredIcons = widget.allIcons;
|
||||
_showSearchBox = _autoFocusSearch;
|
||||
searchBoxFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
searchBoxFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _applyFilteringAndRefresh() {
|
||||
if (_searchText.isEmpty) {
|
||||
setState(() {
|
||||
_filteredIcons = widget.allIcons;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final filteredIcons = <String, AllIconData>{};
|
||||
widget.allIcons.forEach((title, iconData) {
|
||||
if (title.toLowerCase().contains(_searchText.toLowerCase())) {
|
||||
filteredIcons[title] = iconData;
|
||||
}
|
||||
});
|
||||
|
||||
setState(() {
|
||||
_filteredIcons = filteredIcons;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: !_showSearchBox
|
||||
? const Text('Choose icon')
|
||||
: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofocus: _autoFocusSearch,
|
||||
controller: _textController,
|
||||
onChanged: (value) {
|
||||
_searchText = value;
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.searchHint,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
),
|
||||
focusNode: searchBoxFocusNode,
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: _showSearchBox
|
||||
? const Icon(Icons.clear)
|
||||
: const Icon(Icons.search),
|
||||
tooltip: l10n.search,
|
||||
onPressed: () {
|
||||
setState(
|
||||
() {
|
||||
_showSearchBox = !_showSearchBox;
|
||||
if (!_showSearchBox) {
|
||||
_textController.clear();
|
||||
_searchText = "";
|
||||
} else {
|
||||
_searchText = _textController.text;
|
||||
|
||||
// Request focus on the search box
|
||||
searchBoxFocusNode.requestFocus();
|
||||
}
|
||||
_applyFilteringAndRefresh();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 90)
|
||||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
crossAxisSpacing: 14,
|
||||
mainAxisSpacing: 14,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: _filteredIcons.length,
|
||||
itemBuilder: (context, index) {
|
||||
final title = _filteredIcons.keys.elementAt(index);
|
||||
final iconData = _filteredIcons[title]!;
|
||||
IconType iconType = iconData.type;
|
||||
String? color = iconData.color;
|
||||
String? slug = iconData.slug;
|
||||
Widget iconWidget;
|
||||
if (iconType == IconType.simpleIcon) {
|
||||
final simpleIconPath = normalizeSimpleIconName(title);
|
||||
iconWidget = IconUtils.instance.getSVGIcon(
|
||||
"assets/simple-icons/icons/$simpleIconPath.svg",
|
||||
title,
|
||||
color,
|
||||
40,
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
iconWidget = IconUtils.instance.getSVGIcon(
|
||||
"assets/custom-icons/icons/${slug ?? title}.svg",
|
||||
title,
|
||||
color,
|
||||
40,
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey(title),
|
||||
onTap: () {
|
||||
final newIcon = AllIconData(
|
||||
title: title,
|
||||
type: iconType,
|
||||
color: color,
|
||||
slug: slug,
|
||||
);
|
||||
Navigator.of(context).pop(newIcon);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1.5,
|
||||
color: title.toLowerCase() ==
|
||||
widget.currentIcon.toLowerCase()
|
||||
? getEnteColorScheme(context)
|
||||
.tagChipSelectedColor
|
||||
: Colors.transparent,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12.0),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: iconWidget,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: title.toLowerCase() ==
|
||||
widget.currentIcon.toLowerCase()
|
||||
? const EdgeInsets.only(left: 2, right: 2)
|
||||
: const EdgeInsets.all(0.0),
|
||||
child: Text(
|
||||
'${title[0].toUpperCase()}${title.substring(1)}',
|
||||
style: getEnteTextTheme(context).mini,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
import 'package:ente_auth/ui/account/logout_dialog.dart';
|
||||
import 'package:ente_auth/ui/code_error_widget.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
@@ -217,10 +218,11 @@ class _HomePageState extends State<HomePage> {
|
||||
void sortFilteredCodes(List<Code> codes, CodeSortKey sortKey) {
|
||||
switch (sortKey) {
|
||||
case CodeSortKey.issuerName:
|
||||
codes.sort((a, b) => a.issuer.compareTo(b.issuer));
|
||||
codes.sort((a, b) => compareAsciiLowerCaseNatural(a.issuer, b.issuer));
|
||||
break;
|
||||
case CodeSortKey.accountName:
|
||||
codes.sort((a, b) => a.account.compareTo(b.account));
|
||||
codes
|
||||
.sort((a, b) => compareAsciiLowerCaseNatural(a.account, b.account));
|
||||
break;
|
||||
case CodeSortKey.mostFrequentlyUsed:
|
||||
codes.sort((a, b) => b.display.tapCount.compareTo(a.display.tapCount));
|
||||
@@ -353,7 +355,7 @@ class _HomePageState extends State<HomePage> {
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: !_showSearchBox
|
||||
? const Text('Ente Auth')
|
||||
? const Text('Ente Auth', style: brandStyleMedium)
|
||||
: TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/services/passkey_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
@@ -20,10 +19,12 @@ import 'package:url_launcher/url_launcher_string.dart';
|
||||
class PasskeyPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String totp2FASessionID;
|
||||
final String accountsUrl;
|
||||
|
||||
const PasskeyPage(
|
||||
this.sessionID, {
|
||||
required this.totp2FASessionID,
|
||||
required this.accountsUrl,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@@ -47,9 +48,8 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
}
|
||||
|
||||
Future<void> launchPasskey() async {
|
||||
final String accountsUrl = PasskeyService.instance.accountsUrl;
|
||||
await launchUrlString(
|
||||
"$accountsUrl/passkeys/verify?"
|
||||
"${widget.accountsUrl}/passkeys/verify?"
|
||||
"passkeySessionID=${widget.sessionID}"
|
||||
"&redirect=enteauth://passkey"
|
||||
"&clientPackage=io.ente.auth",
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -16,71 +17,71 @@ class ReorderCodesPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ReorderCodesPageState extends State<ReorderCodesPage> {
|
||||
int selectedSortOption = 2;
|
||||
bool hasChanged = false;
|
||||
final logger = Logger('ReorderCodesPage');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isCompactMode = PreferenceService.instance.isCompactMode();
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (!didPop) {
|
||||
final hasSaved = await saveUpadedIndexes();
|
||||
if (hasSaved) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Custom order"),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.editOrder),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () async {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final hasSaved = await saveUpadedIndexes();
|
||||
if (hasSaved) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
child: Text(
|
||||
context.l10n.save,
|
||||
style: TextStyle(
|
||||
color: hasChanged
|
||||
? getEnteColorScheme(context).textBase
|
||||
: getEnteColorScheme(context).strokeMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: ReorderableListView(
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator:
|
||||
(Widget child, int index, Animation<double> animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, _) {
|
||||
final animValue = Curves.easeInOut.transform(animation.value);
|
||||
final scale = lerpDouble(1, 1.05, animValue)!;
|
||||
return Transform.scale(scale: scale, child: child);
|
||||
},
|
||||
);
|
||||
},
|
||||
children: [
|
||||
for (final code in widget.codes)
|
||||
selectedSortOption == 2
|
||||
? ReorderableDragStartListener(
|
||||
key: ValueKey('${code.hashCode}_${code.generatedID}'),
|
||||
index: widget.codes.indexOf(code),
|
||||
child: CodeWidget(
|
||||
key: ValueKey(code.generatedID),
|
||||
code,
|
||||
isCompactMode: isCompactMode,
|
||||
),
|
||||
)
|
||||
: CodeWidget(
|
||||
key: ValueKey('${code.hashCode}_${code.generatedID}'),
|
||||
code,
|
||||
isCompactMode: isCompactMode,
|
||||
),
|
||||
],
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (selectedSortOption == 2) updateCodeIndex(oldIndex, newIndex);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ReorderableListView(
|
||||
buildDefaultDragHandles: false,
|
||||
proxyDecorator: (Widget child, int index, Animation<double> animation) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, _) {
|
||||
final animValue = Curves.easeInOut.transform(animation.value);
|
||||
final scale = lerpDouble(1, 1.05, animValue)!;
|
||||
return Transform.scale(scale: scale, child: child);
|
||||
},
|
||||
);
|
||||
},
|
||||
children: [
|
||||
for (final code in widget.codes)
|
||||
ReorderableDragStartListener(
|
||||
key: ValueKey('${code.hashCode}_${code.generatedID}'),
|
||||
index: widget.codes.indexOf(code),
|
||||
child: CodeWidget(
|
||||
key: ValueKey(code.generatedID),
|
||||
code,
|
||||
isCompactMode: isCompactMode,
|
||||
),
|
||||
),
|
||||
],
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
updateCodeIndex(oldIndex, newIndex);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -97,6 +98,7 @@ class _ReorderCodesPageState extends State<ReorderCodesPage> {
|
||||
if (oldIndex < newIndex) newIndex -= 1;
|
||||
final Code code = widget.codes.removeAt(oldIndex);
|
||||
widget.codes.insert(newIndex, code);
|
||||
hasChanged = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/deduplication_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart';
|
||||
import 'package:ente_auth/ui/settings/data/export_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import_page.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -53,6 +58,35 @@ class DataSectionWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.duplicateCodes,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final List<DuplicateCodes> duplicateCodes =
|
||||
await DeduplicationService.instance.getDuplicateCodes();
|
||||
if (duplicateCodes.isEmpty) {
|
||||
unawaited(
|
||||
showChoiceDialog(
|
||||
context,
|
||||
title: l10n.noDuplicates,
|
||||
firstButtonLabel: "OK",
|
||||
secondButtonLabel: null,
|
||||
body: l10n.youveNoDuplicateCodesThatCanBeCleared,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await routeToPage(
|
||||
context,
|
||||
DuplicateCodePage(duplicateCodes: duplicateCodes),
|
||||
);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
|
||||
259
auth/lib/ui/settings/data/duplicate_code_page.dart
Normal file
@@ -0,0 +1,259 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/deduplication_service.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class DuplicateCodePage extends StatefulWidget {
|
||||
final List<DuplicateCodes> duplicateCodes;
|
||||
const DuplicateCodePage({
|
||||
super.key,
|
||||
required this.duplicateCodes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DuplicateCodePage> createState() => _DuplicateCodePageState();
|
||||
}
|
||||
|
||||
class _DuplicateCodePageState extends State<DuplicateCodePage> {
|
||||
final Logger _logger = Logger("DuplicateCodePage");
|
||||
late List<DuplicateCodes> _duplicateCodes;
|
||||
final Set<int> selectedGrids = <int>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_duplicateCodes = widget.duplicateCodes;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.deduplicateCodes),
|
||||
elevation: 0,
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (selectedGrids.length == _duplicateCodes.length) {
|
||||
_removeAllGrids();
|
||||
} else {
|
||||
_selectAllGrids();
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
selectedGrids.length == _duplicateCodes.length
|
||||
? l10n.deselectAll
|
||||
: l10n.selectAll,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context)
|
||||
.iconTheme
|
||||
.color!
|
||||
.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.only(left: 4)),
|
||||
selectedGrids.length == _duplicateCodes.length
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
size: 24,
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _duplicateCodes.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final List<Code> codes = _duplicateCodes[index].codes;
|
||||
return _getGridView(
|
||||
codes,
|
||||
index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
selectedGrids.isEmpty ? const SizedBox.shrink() : _getDeleteButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getGridView(List<Code> code, int itemIndex) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (selectedGrids.contains(itemIndex)) {
|
||||
selectedGrids.remove(itemIndex);
|
||||
} else {
|
||||
selectedGrids.add(itemIndex);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"${code[0].issuer}, ${code.length} items",
|
||||
),
|
||||
!selectedGrids.contains(itemIndex)
|
||||
? Icon(
|
||||
Icons.check_circle_outlined,
|
||||
color: getEnteColorScheme(context).strokeMuted,
|
||||
size: 24,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.check_circle,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AlignedGridView.count(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
|
||||
.clamp(1, double.infinity)
|
||||
.toInt(),
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return CodeWidget(
|
||||
key: ValueKey('${code.hashCode}_$index'),
|
||||
code[index],
|
||||
isCompactMode: false,
|
||||
);
|
||||
},
|
||||
itemCount: code.length,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getDeleteButton() {
|
||||
int selectedItemsCount = 0;
|
||||
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
|
||||
if (selectedGrids.contains(idx)) {
|
||||
selectedItemsCount += _duplicateCodes[idx].codes.length - 1;
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 20),
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await deleteDuplicates(selectedItemsCount);
|
||||
},
|
||||
child: Text(
|
||||
"Delete $selectedItemsCount items",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectAllGrids() {
|
||||
selectedGrids.clear();
|
||||
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
|
||||
selectedGrids.add(idx);
|
||||
}
|
||||
}
|
||||
|
||||
void _removeAllGrids() {
|
||||
selectedGrids.clear();
|
||||
}
|
||||
|
||||
Future<void> deleteDuplicates(int itemCount) async {
|
||||
bool isAuthSuccessful =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
context.l10n.deleteCodeAuthMessage,
|
||||
);
|
||||
if (!isAuthSuccessful) {
|
||||
return;
|
||||
}
|
||||
FocusScope.of(context).requestFocus();
|
||||
final l10n = context.l10n;
|
||||
final String message = "Are you sure you want to trash $itemCount items?";
|
||||
await showChoiceActionSheet(
|
||||
context,
|
||||
title: l10n.deleteDuplicates,
|
||||
body: message,
|
||||
firstButtonLabel: l10n.trash,
|
||||
isCritical: true,
|
||||
firstButtonOnTap: () async {
|
||||
try {
|
||||
for (int idx = 0; idx < _duplicateCodes.length; idx++) {
|
||||
if (selectedGrids.contains(idx)) {
|
||||
final List<Code> codes = _duplicateCodes[idx].codes;
|
||||
for (int i = 1; i < codes.length; i++) {
|
||||
final display = codes[i].display;
|
||||
final Code code = codes[i].copyWith(
|
||||
display: display.copyWith(trashed: true),
|
||||
);
|
||||
await CodeStore.instance.addCode(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
_logger.severe('Failed to trash duplicate codes: ${e.toString()}');
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/export/ente.dart';
|
||||
@@ -9,16 +8,15 @@ import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/settings/data/html_export.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/share_utils.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
Future<void> handleExportClick(BuildContext context) async {
|
||||
@@ -41,13 +39,22 @@ Future<void> handleExportClick(BuildContext context) async {
|
||||
isInAlert: true,
|
||||
buttonAction: ButtonAction.second,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.plainHTML,
|
||||
buttonSize: ButtonSize.large,
|
||||
isInAlert: true,
|
||||
buttonAction: ButtonAction.third,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result?.action != null && result!.action != ButtonAction.cancel) {
|
||||
if (result.action == ButtonAction.first) {
|
||||
await _requestForEncryptionPassword(context);
|
||||
} else {
|
||||
await _showExportWarningDialog(context);
|
||||
} else if (result.action == ButtonAction.second) {
|
||||
await _showExportWarningDialog(context, "txt");
|
||||
} else if (result.action == ButtonAction.third) {
|
||||
await _showExportWarningDialog(context, "html");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,9 +105,8 @@ Future<void> _requestForEncryptionPassword(
|
||||
),
|
||||
);
|
||||
// get json value of data
|
||||
await _exportCodes(context, jsonEncode(data.toJson()));
|
||||
} catch (e, s) {
|
||||
Logger("ExportWidget").severe(e, s);
|
||||
await _exportCodes(context, jsonEncode(data.toJson()), "txt");
|
||||
} catch (e) {
|
||||
showToast(context, "Error while exporting codes.");
|
||||
}
|
||||
}
|
||||
@@ -108,26 +114,34 @@ Future<void> _requestForEncryptionPassword(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showExportWarningDialog(BuildContext context) async {
|
||||
Future<void> _showExportWarningDialog(BuildContext context, String type) async {
|
||||
await showChoiceActionSheet(
|
||||
context,
|
||||
title: context.l10n.warning,
|
||||
body: context.l10n.exportWarningDesc,
|
||||
isCritical: true,
|
||||
firstButtonOnTap: () async {
|
||||
final data = await _getAuthDataForExport();
|
||||
await _exportCodes(context, data);
|
||||
if (type == "html") {
|
||||
final data = await generateHtml(context);
|
||||
await _exportCodes(context, data, type);
|
||||
} else {
|
||||
final data = await _getAuthDataForExport();
|
||||
await _exportCodes(context, data, type);
|
||||
}
|
||||
},
|
||||
secondButtonLabel: context.l10n.cancel,
|
||||
firstButtonLabel: context.l10n.iUnderStand,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportCodes(BuildContext context, String fileContent) async {
|
||||
Future<void> _exportCodes(
|
||||
BuildContext context,
|
||||
String fileContent,
|
||||
String extension,
|
||||
) async {
|
||||
DateTime now = DateTime.now().toUtc();
|
||||
String formattedDate = DateFormat('yyyy-MM-dd').format(now);
|
||||
String exportFileName = 'ente-auth-codes-$formattedDate';
|
||||
String exportFileExtension = 'txt';
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(context, context.l10n.authToExportCodes);
|
||||
await PlatformUtil.refocusWindows();
|
||||
@@ -142,14 +156,14 @@ Future<void> _exportCodes(BuildContext context, String fileContent) async {
|
||||
saveAction: () async {
|
||||
await PlatformUtil.shareFile(
|
||||
exportFileName,
|
||||
exportFileExtension,
|
||||
extension,
|
||||
CryptoUtil.strToBin(fileContent),
|
||||
MimeType.text,
|
||||
);
|
||||
},
|
||||
sendAction: () async {
|
||||
final codeFile = File(
|
||||
"${Configuration.instance.getTempDirectory()}$exportFileName.$exportFileExtension",
|
||||
"${Configuration.instance.getTempDirectory()}$exportFileName.$extension",
|
||||
);
|
||||
if (codeFile.existsSync()) {
|
||||
await codeFile.delete();
|
||||
|
||||
264
auth/lib/ui/settings/data/html_export.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
Future<String> generateQRImageBase64(String data) async {
|
||||
final qrPainter = QrPainter(
|
||||
data: data,
|
||||
version: QrVersions.auto,
|
||||
eyeStyle: const QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: Colors.black,
|
||||
),
|
||||
dataModuleStyle: const QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
|
||||
const size = 250.0;
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
qrPainter.paint(canvas, const Size(size, size));
|
||||
final picture = recorder.endRecording();
|
||||
final img = await picture.toImage(size.toInt(), size.toInt());
|
||||
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
final pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
return base64Encode(pngBytes);
|
||||
}
|
||||
|
||||
Future<String> generateOTPEntryHtml(Code code, BuildContext context) async {
|
||||
final qrBase64 = await generateQRImageBase64(code.rawData);
|
||||
String notes = code.display.note;
|
||||
if (notes.isNotEmpty) {
|
||||
notes = '<p class="group">Note: <b>$notes</b></p>';
|
||||
}
|
||||
return '''
|
||||
<table class="otp-entry">
|
||||
<tr>
|
||||
<td>
|
||||
<p><b>${code.issuer}</b></p>
|
||||
<p><b>${code.account}</b></p>
|
||||
<p class="group">Type: <b>${code.type.name}</b></p>
|
||||
<p>Algorithm: <b>${code.algorithm.name}</b></p>
|
||||
<p>Digits: <b>${code.digits}</b></p>
|
||||
<p>Secret: <b>${code.secret}</b></p>
|
||||
$notes
|
||||
</td>
|
||||
<td class="otp-qr">
|
||||
<img src="data:image/png;base64,$qrBase64" alt="QR Code">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
<hr class="red-separator" />
|
||||
<br/>
|
||||
''';
|
||||
}
|
||||
|
||||
Future<String> generateHtml(BuildContext context) async {
|
||||
DateTime now = DateTime.now().toUtc();
|
||||
String formattedDate = DateFormat('d MMMM, yyyy').format(now);
|
||||
final allCodes = await CodeStore.instance.getAllCodes();
|
||||
final List<String> enteries = [];
|
||||
|
||||
for (final code in allCodes) {
|
||||
if (code.hasError) continue;
|
||||
final entry = await generateOTPEntryHtml(code, context);
|
||||
enteries.add(entry);
|
||||
}
|
||||
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<meta content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
background-color: #f0f1f3;
|
||||
font-family: "Helvetica Neue", "Segoe UI", Helvetica, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 27px;
|
||||
margin: 0;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f4f4f4f4;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
border-color: #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: #0055d4;
|
||||
border-radius: 3px;
|
||||
text-decoration: none !important;
|
||||
color: #fff !important;
|
||||
font-weight: bold;
|
||||
padding: 10px 30px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #888;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0055d4;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.otp-entry {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.otp-entry td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.otp-qr img {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-icons {
|
||||
padding: 4px !important;
|
||||
width: 24px !important;
|
||||
}
|
||||
|
||||
.otp-entry {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.otp-entry td {
|
||||
padding: 20px;
|
||||
margin: 0px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.otp-entry td:first-child {
|
||||
width: 70%;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.otp-qr img {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.otp-entry td.otp-qr {
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.otp-entry p {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.otp-entry p.group {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
hr.red-separator {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: rgb(173, 0, 255);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align: center;">Ente Auth</h1>
|
||||
<h4 style="text-align: center; margin-bottom: 5px;">OTP Data Export</h4>
|
||||
<p style="text-align: center; margin-top: 0px;">$formattedDate</p>
|
||||
<div class="gutter" style="padding: 4px"> </div>
|
||||
<div class="wrap" style=" background-color: rgb(255, 255, 255); padding: 2px
|
||||
30px 30px 30px; max-width: 700px; margin: 0 auto; border-radius: 5px;
|
||||
font-size: 16px; ">
|
||||
<main>
|
||||
<p>
|
||||
${enteries.join('')}
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="footer" style="text-align: center; font-size: 12px; color:
|
||||
rgb(136, 136, 136)">
|
||||
<div>
|
||||
<a href="https://ente.io" target="_blank"><img src="https://email-assets.ente.io/ente-green.png" style="width: 100px;
|
||||
padding: 24px" title="Ente" alt="Ente" /></a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://fosstodon.org/@ente" target="_blank"><img src="https://email-assets.ente.io/mastodon-icon.png"
|
||||
class="footer-icons" style="width: 24px; padding: 4px" title="Mastodon" alt="Mastodon" /></a>
|
||||
<a href="https://twitter.com/enteio" target="_blank"><img src="https://email-assets.ente.io/twitter-icon.png"
|
||||
class="footer-icons" style="width: 24px; padding: 4px" title="Twitter" alt="Twitter" /></a>
|
||||
<a href="https://discord.ente.io" target="_blank"><img src="https://email-assets.ente.io/discord-icon.png"
|
||||
class="footer-icons" style="width: 24px; padding: 4px" title="Discord" alt="Discord" /></a>
|
||||
<a href="https://github.com/ente-io" target="_blank"><img src="https://email-assets.ente.io/github-icon.png"
|
||||
class="footer-icons" style="width: 24px; padding: 4px" title="GitHub" alt="GitHub" /></a>
|
||||
</div>
|
||||
<p>
|
||||
Ente Technologies, Inc.
|
||||
<br /> 1111B S Governors Ave 6032 Dover, DE 19904
|
||||
</p>
|
||||
<br />
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
''';
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class _SupportSectionWidgetState extends State<SupportSectionWidget> {
|
||||
onTap: () async {
|
||||
// ignore: unawaited_futures
|
||||
launchUrlString(
|
||||
githubIssuesUrl,
|
||||
githubFeatureRequestUrl,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ThemeSwitchWidget extends StatefulWidget {
|
||||
const ThemeSwitchWidget({super.key});
|
||||
@@ -42,7 +40,7 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandableMenuItemWidget(
|
||||
title: "Theme",
|
||||
title: context.l10n.theme,
|
||||
selectionOptionsWidget: _getSectionOptions(context),
|
||||
leadingIcon: Theme.of(context).brightness == Brightness.light
|
||||
? Icons.light_mode_outlined
|
||||
@@ -64,10 +62,21 @@ class _ThemeSwitchWidgetState extends State<ThemeSwitchWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
String _name(BuildContext ctx, AdaptiveThemeMode mode) {
|
||||
switch (mode) {
|
||||
case AdaptiveThemeMode.light:
|
||||
return ctx.l10n.lightTheme;
|
||||
case AdaptiveThemeMode.dark:
|
||||
return ctx.l10n.darkTheme;
|
||||
case AdaptiveThemeMode.system:
|
||||
return ctx.l10n.systemTheme;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _menuItem(BuildContext context, AdaptiveThemeMode themeMode) {
|
||||
return MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: toBeginningOfSentenceCase(themeMode.name)!,
|
||||
title: _name(context, themeMode),
|
||||
textStyle: Theme.of(context).colorScheme.enteTheme.textTheme.body,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/all_icon_data.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -24,6 +25,80 @@ class IconUtils {
|
||||
await _loadJson();
|
||||
}
|
||||
|
||||
Map<String, AllIconData> getAllIcons() {
|
||||
Set<String> processedIconPaths = {};
|
||||
final allIcons = <String, AllIconData>{};
|
||||
|
||||
final simpleIterator = _simpleIcons.entries.iterator;
|
||||
final customIterator = _customIcons.entries.iterator;
|
||||
|
||||
var simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
|
||||
var customEntry = customIterator.moveNext() ? customIterator.current : null;
|
||||
|
||||
String simpleIconPath, customIconPath;
|
||||
|
||||
while (simpleEntry != null && customEntry != null) {
|
||||
if (simpleEntry.key.compareTo(customEntry.key) <= 0) {
|
||||
simpleIconPath = "assets/simple-icons/icons/${simpleEntry.key}.svg";
|
||||
if (!processedIconPaths.contains(simpleIconPath)) {
|
||||
allIcons[simpleEntry.key] = AllIconData(
|
||||
title: simpleEntry.key,
|
||||
type: IconType.simpleIcon,
|
||||
color: simpleEntry.value,
|
||||
);
|
||||
processedIconPaths.add(simpleIconPath);
|
||||
}
|
||||
simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
|
||||
} else {
|
||||
customIconPath =
|
||||
"assets/custom-icons/icons/${customEntry.value.slug ?? customEntry.key}.svg";
|
||||
|
||||
if (!processedIconPaths.contains(customIconPath)) {
|
||||
allIcons[customEntry.key] = AllIconData(
|
||||
title: customEntry.key,
|
||||
type: IconType.customIcon,
|
||||
color: customEntry.value.color,
|
||||
slug: customEntry.value.slug,
|
||||
);
|
||||
processedIconPaths.add(customIconPath);
|
||||
}
|
||||
customEntry = customIterator.moveNext() ? customIterator.current : null;
|
||||
}
|
||||
}
|
||||
|
||||
while (simpleEntry != null) {
|
||||
simpleIconPath = "assets/simple-icons/icons/${simpleEntry.key}.svg";
|
||||
|
||||
if (!processedIconPaths.contains(simpleIconPath)) {
|
||||
allIcons[simpleEntry.key] = AllIconData(
|
||||
title: simpleEntry.key,
|
||||
type: IconType.simpleIcon,
|
||||
color: simpleEntry.value,
|
||||
);
|
||||
processedIconPaths.add(simpleIconPath);
|
||||
}
|
||||
simpleEntry = simpleIterator.moveNext() ? simpleIterator.current : null;
|
||||
}
|
||||
|
||||
while (customEntry != null) {
|
||||
customIconPath =
|
||||
"assets/custom-icons/icons/${customEntry.value.slug ?? customEntry.key}.svg";
|
||||
|
||||
if (!processedIconPaths.contains(customIconPath)) {
|
||||
allIcons[customEntry.key] = AllIconData(
|
||||
title: customEntry.key,
|
||||
type: IconType.customIcon,
|
||||
color: customEntry.value.color,
|
||||
slug: customEntry.value.slug,
|
||||
);
|
||||
processedIconPaths.add(customIconPath);
|
||||
}
|
||||
customEntry = customIterator.moveNext() ? customIterator.current : null;
|
||||
}
|
||||
|
||||
return allIcons;
|
||||
}
|
||||
|
||||
Widget getIcon(
|
||||
BuildContext context,
|
||||
String provider, {
|
||||
@@ -38,7 +113,7 @@ class IconUtils {
|
||||
);
|
||||
for (final title in titlesList) {
|
||||
if (_customIcons.containsKey(title)) {
|
||||
return _getSVGIcon(
|
||||
return getSVGIcon(
|
||||
"assets/custom-icons/icons/${_customIcons[title]!.slug ?? title}.svg",
|
||||
title,
|
||||
_customIcons[title]!.color,
|
||||
@@ -46,8 +121,9 @@ class IconUtils {
|
||||
context,
|
||||
);
|
||||
} else if (_simpleIcons.containsKey(title)) {
|
||||
return _getSVGIcon(
|
||||
"assets/simple-icons/icons/$title.svg",
|
||||
final simpleIconPath = normalizeSimpleIconName(title);
|
||||
return getSVGIcon(
|
||||
"assets/simple-icons/icons/$simpleIconPath.svg",
|
||||
title,
|
||||
_simpleIcons[title],
|
||||
width,
|
||||
@@ -75,7 +151,7 @@ class IconUtils {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getSVGIcon(
|
||||
Widget getSVGIcon(
|
||||
String path,
|
||||
String title,
|
||||
String? color,
|
||||
@@ -124,7 +200,7 @@ class IconUtils {
|
||||
final simpleIconData = await rootBundle
|
||||
.loadString('assets/simple-icons/_data/simple-icons.json');
|
||||
final simpleIcons = json.decode(simpleIconData);
|
||||
for (final icon in simpleIcons["icons"]) {
|
||||
for (final icon in simpleIcons) {
|
||||
_simpleIcons[icon["title"]
|
||||
.toString()
|
||||
.replaceAll(' ', '')
|
||||
@@ -145,14 +221,14 @@ class IconUtils {
|
||||
for (final name in icon["altNames"]) {
|
||||
_customIcons[name.toString().replaceAll(' ', '').toLowerCase()] =
|
||||
CustomIconData(
|
||||
icon["slug"],
|
||||
icon["slug"] ?? ((icon["title"] as String).toLowerCase()),
|
||||
icon["hex"],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger("IconUtils").severe("Error loading icons", e);
|
||||
} catch (e, s) {
|
||||
Logger("IconUtils").severe("Error loading icons", e, s);
|
||||
if (kDebugMode) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -170,3 +246,43 @@ class CustomIconData {
|
||||
|
||||
CustomIconData(this.slug, this.color);
|
||||
}
|
||||
|
||||
final charMap = {
|
||||
'á': 'a',
|
||||
'à': 'a',
|
||||
'â': 'a',
|
||||
'ä': 'a',
|
||||
'é': 'e',
|
||||
'è': 'e',
|
||||
'ê': 'e',
|
||||
'ë': 'e',
|
||||
'í': 'i',
|
||||
'ì': 'i',
|
||||
'î': 'i',
|
||||
'ï': 'i',
|
||||
'ó': 'o',
|
||||
'ò': 'o',
|
||||
'ô': 'o',
|
||||
'ö': 'o',
|
||||
'ú': 'u',
|
||||
'ù': 'u',
|
||||
'û': 'u',
|
||||
'ü': 'u',
|
||||
'ç': 'c',
|
||||
'ñ': 'n',
|
||||
'.': 'dot',
|
||||
'-': '',
|
||||
'&': 'and',
|
||||
'+': 'plus',
|
||||
':': '',
|
||||
"'": '',
|
||||
'/': '',
|
||||
'!': '',
|
||||
};
|
||||
String normalizeSimpleIconName(String input) {
|
||||
final buffer = StringBuffer();
|
||||
for (var char in input.characters) {
|
||||
buffer.write(charMap[char] ?? char);
|
||||
}
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
|
||||
@@ -174,4 +174,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: f401c31c8f7c5571f6f565c78915d54338812dab
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -429,6 +429,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -42,10 +42,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
|
||||
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.3.3"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -431,6 +431,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
figma_squircle:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: figma_squircle
|
||||
sha256: "790b91a9505e90d246f6efe2fa065ff7fffe658c7b44fe9b5b20c7b0ad3818c0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -520,10 +528,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_inappwebview
|
||||
sha256: "93cfcca02bdda4b26cd700cf70d9ddba09d8348e3e8f2857638c23ed23a4fcb4"
|
||||
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.1.5"
|
||||
flutter_inappwebview_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -576,10 +584,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_windows
|
||||
sha256: "95ebc65aecfa63b2084c822aec6ba0545f0a0afaa3899f2c752ec96c09108db5"
|
||||
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0+2"
|
||||
version: "0.6.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -977,10 +985,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1647,10 +1655,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1660,7 +1668,7 @@ packages:
|
||||
source: hosted
|
||||
version: "6.3.11"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 4.1.6+416
|
||||
version: 4.2.2+422
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -8,7 +8,7 @@ environment:
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.1.0 # done
|
||||
app_links: ^6.2.1
|
||||
app_links: ^6.3.3
|
||||
archive: ^3.3.7
|
||||
auto_size_text: ^3.0.0
|
||||
base32: ^2.1.3
|
||||
@@ -31,6 +31,7 @@ dependencies:
|
||||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^3.0.0
|
||||
ffi: ^2.1.0
|
||||
figma_squircle: ^0.5.3
|
||||
file_picker: ^8.1.2
|
||||
# https://github.com/incrediblezayed/file_saver/issues/86
|
||||
file_saver: ^0.2.11
|
||||
@@ -43,7 +44,7 @@ dependencies:
|
||||
flutter_context_menu: ^0.2.0
|
||||
flutter_displaymode: ^0.6.0
|
||||
flutter_email_sender: ^6.0.2
|
||||
flutter_inappwebview: ^6.0.0
|
||||
flutter_inappwebview: ^6.1.5
|
||||
flutter_launcher_icons: ^0.14.1
|
||||
flutter_local_authentication:
|
||||
git:
|
||||
@@ -97,7 +98,8 @@ dependencies:
|
||||
styled_text: ^8.1.0
|
||||
tray_manager: ^0.2.1
|
||||
tuple: ^2.0.0
|
||||
url_launcher: ^6.1.5
|
||||
url_launcher: ^6.3.1
|
||||
url_launcher_ios: ^6.3.1
|
||||
uuid: ^4.2.2
|
||||
win32: ^5.1.1
|
||||
window_manager: ^0.4.2
|
||||
|
||||
@@ -72,7 +72,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Point origin(70, 70);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"Ente Auth", origin, size))
|
||||
{
|
||||
|
||||
@@ -11,7 +11,14 @@
|
||||
|
||||
import { nativeImage, shell } from "electron/common";
|
||||
import type { WebContents } from "electron/main";
|
||||
import { BrowserWindow, Menu, Tray, app, protocol } from "electron/main";
|
||||
import {
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
Tray,
|
||||
app,
|
||||
dialog,
|
||||
protocol,
|
||||
} from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
@@ -131,6 +138,7 @@ const main = () => {
|
||||
const webContents = mainWindow.webContents;
|
||||
setDownloadPath(webContents);
|
||||
allowExternalLinks(webContents);
|
||||
handleBackOnStripeCheckout(mainWindow);
|
||||
allowAllCORSOrigins(webContents);
|
||||
|
||||
// Start loading the renderer.
|
||||
@@ -502,6 +510,45 @@ const allowExternalLinks = (webContents: WebContents) =>
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle back button presses on the Stripe checkout page.
|
||||
*
|
||||
* For payments, we show the Stripe checkout page to the user in the app's
|
||||
* window. On this page there is a back button that allows the user to get back
|
||||
* to the app's contents. Since we're not showing the browser controls, this is
|
||||
* the only way to get back to the app.
|
||||
*
|
||||
* If the user enters something in the text fields on this page (e.g. if they
|
||||
* start entering their credit card number), and then press back, then the
|
||||
* browser shows the user a dialog asking them to confirm if they want to
|
||||
* discard their unsaved changes. However, when running in the context of an
|
||||
* Electron app, this dialog is not shown, and instead the app just gets stuck
|
||||
* (the back button stops working, and quitting the app also doesn't work since
|
||||
* there is an invisible modal dialog).
|
||||
*
|
||||
* So we instead intercept these back button presses, and show the same dialog
|
||||
* that the browser would've shown.
|
||||
*/
|
||||
const handleBackOnStripeCheckout = (window: BrowserWindow) =>
|
||||
window.webContents.on("will-prevent-unload", (event) => {
|
||||
const url = new URL(window.webContents.getURL());
|
||||
// Only intercept on Stripe checkout pages.
|
||||
if (url.host != "checkout.stripe.com") return;
|
||||
|
||||
// The dialog copy is similar to what Chrome would've shown.
|
||||
// https://www.electronjs.org/docs/latest/api/web-contents#event-will-prevent-unload
|
||||
const choice = dialog.showMessageBoxSync(window, {
|
||||
type: "question",
|
||||
buttons: ["Leave", "Stay"],
|
||||
title: "Leave site?",
|
||||
message: "Changes that you made may not be saved.",
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
const leave = choice === 0;
|
||||
if (leave) event.preventDefault();
|
||||
});
|
||||
|
||||
/**
|
||||
* Allow uploads to arbitrary S3 buckets.
|
||||
*
|
||||
|
||||
@@ -22,7 +22,7 @@ export const fsReadTextFile = async (filePath: string) =>
|
||||
fs.readFile(filePath, "utf-8");
|
||||
|
||||
export const fsWriteFile = (path: string, contents: string) =>
|
||||
fs.writeFile(path, contents);
|
||||
fs.writeFile(path, contents, { flush: true });
|
||||
|
||||
export const fsIsDir = async (dirPath: string) => {
|
||||
if (!existsSync(dirPath)) return false;
|
||||
|
||||
@@ -39,6 +39,10 @@ export const sidebar = [
|
||||
link: "/photos/features/free-up-space/",
|
||||
},
|
||||
{ text: "Hidden photos", link: "/photos/features/hide" },
|
||||
{
|
||||
text: "Legacy",
|
||||
link: "/photos/features/legacy/",
|
||||
},
|
||||
{
|
||||
text: "Location tags",
|
||||
link: "/photos/features/location-tags",
|
||||
@@ -254,7 +258,7 @@ export const sidebar = [
|
||||
link: "/self-hosting/guides/configuring-s3",
|
||||
},
|
||||
{
|
||||
text: "Using external S3",
|
||||
text: "Hosting Ente with external S3 (Community)",
|
||||
link: "/self-hosting/guides/external-s3",
|
||||
},
|
||||
{
|
||||
|
||||
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/docs/photos/features/legacy/add_trusted_contact.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
50
docs/docs/photos/features/legacy/index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Legacy
|
||||
description: Using Legacy to pass on your memories to loved ones
|
||||
---
|
||||
|
||||
# Legacy
|
||||
|
||||
Legacy allows trusted contacts to recover your account in your absence. The main usecase here is to pass on your memories after your death. It can also be useful for other cases - for e.g., when you forget your password and recovery key.
|
||||
|
||||
Trusted Contacts can initiate a recovery, and if not blocked in 30 days, would be able to change the password to your account and thereby access your memories.
|
||||
|
||||
## Adding a trusted contact
|
||||
|
||||
You can add a trusted contact for your account using the mobile app for Ente Photos. Go to Settings -> Account -> Legacy, and click on "Add Trusted Contact".
|
||||
|
||||
You would be asked to enter the email address of the trusted contact you want to add or choose from a list of contacts on Ente. Please note that the trusted contact must be an Ente user.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{width=300px}
|
||||
|
||||
</div>
|
||||
|
||||
The trusted contact must accept your request. They can do so by going to Settings -> Account -> Legacy in the Ente Photos mobile app, and clicking on your email address within the Legacy accounts sections. Ente would also send an email notification to the trusted contact to nudge them to accept the invite.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{width=300px}
|
||||
|
||||
</div>
|
||||
|
||||
## Recovering an account as a trusted contact
|
||||
|
||||
As a trusted contact, you can recover an account by going to Settings -> Account -> Legacy in the Ente photos app, tapping on the email address of the account within the Legacy account sections.
|
||||
|
||||
<div align="center">
|
||||
|
||||
{width=300px}
|
||||
|
||||
</div>
|
||||
|
||||
Once the recovery is initiated, the account owner would get 30 days to block the recovery. After 30 days, you can go the same page in the app, where you will be prompted to change the password of the account. Once you change the password, you would be able to access the recovered account with the new password.
|
||||
|
||||
## Blocking account recovery by a trusted contact
|
||||
|
||||
After a trusted contact initiates a recover, you, as the account owner, would get 30 days to block the recovery. To do this, you must go to Settings -> Account -> Legacy, where you will see a message that recovery of the account has been initiated. Tapping on that will allow you to block the recovery.
|
||||
|
||||
## Removing a trusted contact
|
||||
|
||||
You can remove a trusted contact by going to Settings -> Account -> Legacy, tapping on the trusted contact you want to remove, and choosing "Remove" in the popup.
|
||||
BIN
docs/docs/photos/features/legacy/initiate_account_recovery.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -39,6 +39,10 @@ device.
|
||||
> desktop app first, because it can index your existing photos faster. Once your
|
||||
> existing photos have been indexed, then you can use either. The mobile app is
|
||||
> fast enough to index new photos as they are being backed up.
|
||||
>
|
||||
> Also, it is beneficial to enable machine learning before importing your
|
||||
> photos, as this allows the Ente app to index your files as they are getting
|
||||
> uploaded instead of needing to download them again.
|
||||
|
||||
The indexes are synced across all your devices automatically using the same
|
||||
end-to-end encrypted security that we use for syncing your photos.
|
||||
|
||||
@@ -68,3 +68,10 @@ will ignore already backed up files and upload just the rest.
|
||||
|
||||
If you run into any issues during this migration, please reach out to
|
||||
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> In case you wish to use face recognition and other advanced search features
|
||||
> provided by Ente, we recommend that you enable [machine
|
||||
> learning](/photos/features/machine-learning) before importing your photos so
|
||||
> that the Ente app can directly index files as they are getting uploaded.
|
||||
|
||||
@@ -7,6 +7,14 @@ description:
|
||||
|
||||
# Hosting server and web app using external S3
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a community contributed guide, and some of these steps might be out of
|
||||
> sync with the upstream documentation. If something is not working correctly,
|
||||
> please also see the latest
|
||||
> [READMEs](https://github.com/ente-io/ente/blob/main/server/README.md) in the
|
||||
> repository and/or other guides in [self-hosting](/self-hosting/).
|
||||
|
||||
This guide is for self hosting the server and the web application of Ente Photos
|
||||
using docker compose and an external S3 bucket. So we assume that you already
|
||||
have the keys and secrets for the S3 bucket. The plan is as follows:
|
||||
@@ -17,14 +25,6 @@ have the keys and secrets for the S3 bucket. The plan is as follows:
|
||||
4. Create an account and increase storage quota
|
||||
5. Fix potential CORS issue with your bucket
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a community contributed guide, and some of these steps might be out of
|
||||
> sync with the upstream documentation. If something is not working correctly,
|
||||
> please also see the latest
|
||||
> [READMEs](https://github.com/ente-io/ente/blob/main/server/README.md) in the
|
||||
> repository and/or other guides in [self-hosting](/self-hosting/).
|
||||
|
||||
## 1. Create a `compose.yaml` file
|
||||
|
||||
After cloning the main repository with
|
||||
@@ -40,7 +40,6 @@ Create a `compose.yaml` file at the root of the project with the following
|
||||
content (there is nothing to change here):
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
museum:
|
||||
build:
|
||||
@@ -236,9 +235,10 @@ background).
|
||||
|
||||
## 4. Create an account and increase storage quota
|
||||
|
||||
Open `http://localhost:8081` (or the url of your server) in your browser and
|
||||
create an account. Choose 123456 as the value for the one-time token if your
|
||||
email has the correct domain as defined in the `.credentials.env` file.
|
||||
Open `http://localhost:8080` or whatever Endpoint you mentioned for the web app and create an account.
|
||||
If your SMTP related configurations are all set and right, you will receive an email with
|
||||
your OTT in it. There are two work arounds to retrieve the OTP,
|
||||
checkout [this document](https://help.ente.io/self-hosting/faq/otp) for getting your OTT's..
|
||||
|
||||
If you successfully log in, select any plan and increase the storage quota with
|
||||
the following command:
|
||||
@@ -251,6 +251,9 @@ 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.
|
||||
|
||||
@@ -272,14 +275,28 @@ Create a `cors.json` file with the following content:
|
||||
|
||||
You may want to change the `AllowedOrigins` to a more restrictive value.
|
||||
|
||||
Then run the following command with the aws command to update the CORS
|
||||
configuration of your bucket:
|
||||
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 file://cors.json
|
||||
aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/cors.json
|
||||
```
|
||||
|
||||
Upload should now work.
|
||||
### 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> set "cors_allowed_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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Hosting the web app
|
||||
title: Hosting the web apps
|
||||
description:
|
||||
Building and hosting Ente's web app, connecting it to your self-hosted
|
||||
Building and hosting Ente's web apps, connecting it to your self-hosted
|
||||
server
|
||||
---
|
||||
|
||||
@@ -17,47 +17,229 @@ yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
|
||||
```
|
||||
|
||||
This is fine for trying this out and verifying that your self-hosted server is
|
||||
working correctly etc. But if you would like to use the web app for a longer
|
||||
term, then it is recommended that you use a production build.
|
||||
This is fine for trying the web app and verifying that your self-hosted server is
|
||||
working as expected etc. But if you would like to use the web app for a longer term,
|
||||
then it is recommended to follow the Docker approach.
|
||||
|
||||
To create a production build, you can run the same process, but instead do a
|
||||
`yarn build` (which is an alias for `yarn build:photos`). For example,
|
||||
## With Docker/Docker Compose (Recommended)
|
||||
|
||||
```sh
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn build:photos
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This docker image is still in testing stage and it might show up with some
|
||||
> unknown variables in different scenarios. But this image has been tested on a production
|
||||
> ente site.
|
||||
>
|
||||
> 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
|
||||
|
||||
# Configure Albums and Accounts Endpoints
|
||||
ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your-domain.com
|
||||
ENV NEXT_PUBLIC_ENTE_ACCOUNTS_URL=https://your-domain.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}"]
|
||||
```
|
||||
|
||||
This creates a production build, which is a static site consisting of a folder
|
||||
of HTML/CSS/JS files that can then be deployed on any standard web server.
|
||||
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.
|
||||
|
||||
Nginx is a common choice for a web server, and you can then put the generated
|
||||
static site (from the `web/apps/photos/out` folder) to where nginx would serve
|
||||
them. Note that there is nothing specific to nginx here - you can use any web
|
||||
server - the basic gist is that yarn build will produce a web/apps/photos/out
|
||||
folder that you can then serve with any web server of your choice.
|
||||
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.
|
||||
|
||||
If you're new to web development, you might find the [web app's README], and
|
||||
some of the documentation it its source code -
|
||||
[docs/new.md](https://github.com/ente-io/ente/blob/main/web/docs/new.md),
|
||||
[docs/dev.md](https://github.com/ente-io/ente/blob/main/web/docs/dev.md) -
|
||||
useful. We've also documented the process we use for our own production
|
||||
deploypments in
|
||||
[docs/deploy.md](https://github.com/ente-io/ente/blob/main/web/docs/deploy.md),
|
||||
though be aware that that is probably overkill for simple cases.
|
||||
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.
|
||||
|
||||
## Using Docker
|
||||
|
||||
We currently don't offer pre-built Docker images for the web app, however it is
|
||||
quite easy to build and deploy the web app in a Docker container without
|
||||
installing anything extra on your machine. For example, you can use the
|
||||
dockerfile from this
|
||||
[discussion](https://github.com/ente-io/ente/discussions/1183), or use the
|
||||
Dockerfile mentioned in the
|
||||
[notes](https://help.ente.io/self-hosting/guides/external-s3) created by a
|
||||
community member.
|
||||
```sh
|
||||
# Build the image
|
||||
docker build -t <image-name>:<tag> --no-cache --progress plain .
|
||||
```
|
||||
|
||||
## Public sharing
|
||||
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.
|
||||
|
||||
If you'd also like to enable public sharing on the web app you're running,
|
||||
please follow the [step here](https://help.ente.io/self-hosting/faq/sharing).
|
||||
Regarding Albums App, please take a note that they are not web pages 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:
|
||||
- NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your-domain.com
|
||||
- NEXT_PUBLIC_ENTE_ACCOUNTS_URL=https://your-domain.com
|
||||
- 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>
|
||||
```
|
||||
|
||||
Next part is to configure a [web server](#web-server-configuration).
|
||||
|
||||
## Without Docker / Docker compose
|
||||
|
||||
One way to run all the apps together without Docker is by using [PM2](https://pm2.keymetrics.io/)
|
||||
in this setup. The configuration and usage is very simple and just needs one
|
||||
configuration file for it. You can run the apps both in dev server mode as
|
||||
well as static files.
|
||||
|
||||
The below configuration will run the apps in dev server mode.
|
||||
|
||||
### Install PM2
|
||||
|
||||
```sh
|
||||
npm install pm2@latest
|
||||
```
|
||||
|
||||
Copy the below contents to a file called `ecosystem.config.js` inside the `ente/web`
|
||||
directory.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "photos",
|
||||
script: "yarn workspace photos next dev",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
PORT: "3000"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "accounts",
|
||||
script: "yarn workspace accounts next dev",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
PORT: "3001"
|
||||
},
|
||||
{
|
||||
name: "auth",
|
||||
script: "yarn workspace auth next dev",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
PORT: "3002"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "cast",
|
||||
script: "yarn workspace cast next dev",
|
||||
env: {
|
||||
NODE_ENV: "development",
|
||||
PORT: "3003"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
Finally, start pm2.
|
||||
|
||||
```sh
|
||||
pm2 start
|
||||
|
||||
# for logs
|
||||
pm2 logs all
|
||||
```
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -79,11 +79,6 @@ apps and configure them to use your
|
||||
|
||||
## Contributing!
|
||||
|
||||
While we would love to provide a completely seamless self-hosting experience,
|
||||
right now we do not have the engineering bandwidth to answer all queries,
|
||||
document everything exactly etc. We will try (that's why we're writing this!),
|
||||
but we also hope that community members will step up to fill any gaps.
|
||||
|
||||
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
|
||||
|
||||
20
mobile/assets/icons/legacy-dark.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_34053_111310)">
|
||||
<path d="M170 66.7593C170 71.8084 169.042 76.732 167.151 81.3933C165.192 86.2233 162.316 90.5536 158.606 94.265L147.36 105.511L151.657 109.806C152.962 111.112 152.26 113.347 150.441 113.671L128.349 117.598C126.785 117.875 125.423 116.513 125.7 114.949L129.628 92.8595C129.952 91.0413 132.187 90.3393 133.492 91.6442L137.12 95.2717L148.366 84.0261C152.978 79.4144 155.519 73.282 155.519 66.7609C155.519 60.2383 152.978 54.105 148.365 49.4941C143.753 44.8815 137.621 42.3422 131.097 42.3422C124.574 42.3422 118.442 44.8815 113.83 49.4941L107.387 55.936C107.383 55.9064 107.378 55.8784 107.373 55.8488L104.299 38.5628C107.848 35.1847 111.936 32.5447 116.463 30.7097C121.123 28.818 126.048 27.8594 131.097 27.8594C136.146 27.8594 141.07 28.818 145.732 30.7089C150.564 32.667 154.894 35.5421 158.606 39.2528C162.317 42.9643 165.192 47.2946 167.151 52.1246C169.041 56.7859 169.999 61.711 170 66.7593Z" fill="#4AAF3C"/>
|
||||
<path d="M133.814 119.087L105.12 147.779C103.762 149.137 101.92 149.9 99.9998 149.9C98.0791 149.9 96.2375 149.137 94.879 147.779L70.7775 123.678L67.503 126.953C66.1972 128.259 63.9623 127.556 63.6392 125.737L59.7107 103.646C59.4332 102.083 60.7966 100.72 62.3598 100.997L84.4519 104.925C86.2702 105.249 86.9731 107.483 85.6673 108.789L81.0183 113.438L100.001 132.42L124.507 107.914L123.318 114.601C123.053 116.096 123.535 117.63 124.609 118.703C125.684 119.778 127.217 120.259 128.712 119.994L133.814 119.087H133.814Z" fill="#4AAF3C"/>
|
||||
<path d="M102.359 58.8607L80.2668 54.9325C78.4485 54.6095 77.7456 52.3748 79.0514 51.0692L83.1799 46.9412C79.0498 43.9533 74.1009 42.3406 68.9034 42.3406C62.3808 42.3406 56.2477 44.88 51.6363 49.4925C47.0232 54.1042 44.4828 60.2359 44.4828 66.7585C44.4828 73.2812 47.0232 79.4128 51.6363 84.0246L66.9435 99.3301L62.7415 98.5834C61.2462 98.3179 59.7125 98.8 58.6386 99.8738C57.5647 100.948 57.0825 102.482 57.348 103.977L58.697 111.564L41.3955 94.2635C37.6836 90.552 34.8089 86.2217 32.8499 81.3917C30.9588 76.7312 30 71.8076 30 66.7593C30 61.711 30.9588 56.7867 32.8491 52.1254C34.8081 47.2946 37.6836 42.9643 41.3947 39.2536C45.1057 35.5421 49.4365 32.6678 54.267 30.7097C58.9296 28.818 63.8537 27.8594 68.9034 27.8594C73.953 27.8594 78.8771 28.818 83.5389 30.7081C87.158 32.1753 90.4956 34.1565 93.503 36.6183L97.2157 32.9061C98.5215 31.6004 100.756 32.3032 101.079 34.1214L105.007 56.211C105.286 57.7741 103.922 59.1381 102.359 58.8607Z" fill="#4AAF3C"/>
|
||||
</g>
|
||||
<g filter="url(#filter0_f_34053_111310)">
|
||||
<ellipse cx="102.576" cy="170.536" rx="27.5758" ry="1.59085" fill="#1CA609" fill-opacity="0.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_34053_111310" x="70.5" y="164.445" width="64.1515" height="12.1797" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.25" result="effect1_foregroundBlur_34053_111310"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_34053_111310">
|
||||
<rect width="140" height="122" fill="white" transform="translate(30 27.8828)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
20
mobile/assets/icons/legacy-light.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_34053_111286)">
|
||||
<path d="M170 66.7593C170 71.8084 169.042 76.732 167.151 81.3933C165.192 86.2233 162.316 90.5536 158.606 94.265L147.36 105.511L151.657 109.806C152.962 111.112 152.26 113.347 150.441 113.671L128.349 117.598C126.785 117.875 125.423 116.513 125.7 114.949L129.628 92.8594C129.952 91.0413 132.187 90.3393 133.492 91.6441L137.12 95.2717L148.366 84.0261C152.978 79.4144 155.519 73.2819 155.519 66.7609C155.519 60.2383 152.978 54.105 148.365 49.4941C143.753 44.8815 137.621 42.3422 131.097 42.3422C124.574 42.3422 118.442 44.8815 113.83 49.4941L107.387 55.936C107.383 55.9064 107.378 55.8784 107.373 55.8488L104.299 38.5628C107.848 35.1847 111.936 32.5447 116.463 30.7097C121.123 28.818 126.048 27.8594 131.097 27.8594C136.146 27.8594 141.07 28.818 145.732 30.7089C150.564 32.667 154.894 35.5421 158.606 39.2528C162.317 42.9643 165.192 47.2946 167.151 52.1246C169.041 56.7859 169.999 61.7102 170 66.7585V66.7593Z" fill="#4AAF3C"/>
|
||||
<path d="M133.814 119.087L105.12 147.779C103.762 149.137 101.92 149.9 99.9998 149.9C98.0791 149.9 96.2375 149.137 94.879 147.779L70.7775 123.678L67.503 126.953C66.1972 128.259 63.9623 127.556 63.6392 125.737L59.7107 103.646C59.4332 102.083 60.7966 100.72 62.3598 100.997L84.4519 104.925C86.2702 105.249 86.9731 107.483 85.6673 108.789L81.0183 113.438L100.001 132.42L124.507 107.914L123.318 114.601C123.053 116.096 123.535 117.63 124.609 118.703C125.684 119.778 127.217 120.259 128.712 119.994L133.814 119.087H133.814Z" fill="#4AAF3C"/>
|
||||
<path d="M102.359 58.8607L80.2668 54.9325C78.4485 54.6095 77.7456 52.3748 79.0514 51.0692L83.1799 46.9412C79.0498 43.9533 74.1009 42.3406 68.9034 42.3406C62.3808 42.3406 56.2477 44.88 51.6363 49.4925C47.0232 54.1042 44.4828 60.2359 44.4828 66.7585C44.4828 73.2812 47.0232 79.4128 51.6363 84.0246L66.9435 99.3301L62.7415 98.5834C61.2462 98.3179 59.7125 98.8 58.6386 99.8738C57.5647 100.948 57.0825 102.482 57.348 103.977L58.697 111.564L41.3955 94.2635C37.6836 90.552 34.8089 86.2217 32.8499 81.3917C30.9588 76.7312 30 71.8076 30 66.7593C30 61.711 30.9588 56.7867 32.8491 52.1254C34.8081 47.2946 37.6836 42.9643 41.3947 39.2536C45.1057 35.5421 49.4365 32.6678 54.267 30.7097C58.9296 28.818 63.8537 27.8594 68.9034 27.8594C73.953 27.8594 78.8771 28.818 83.5389 30.7081C87.158 32.1753 90.4957 34.1565 93.503 36.6183L97.2157 32.9061C98.5215 31.6004 100.756 32.3032 101.079 34.1214L105.007 56.211C105.286 57.7741 103.922 59.1373 102.359 58.8599V58.8607Z" fill="#4AAF3C"/>
|
||||
</g>
|
||||
<g filter="url(#filter0_f_34053_111286)">
|
||||
<ellipse cx="101.576" cy="170.536" rx="27.5758" ry="1.59085" fill="#E1E1E1" fill-opacity="0.5"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_f_34053_111286" x="69.5" y="164.445" width="64.1515" height="12.1797" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="2.25" result="effect1_foregroundBlur_34053_111286"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_34053_111286">
|
||||
<rect width="140" height="122" fill="white" transform="translate(30 27.8828)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
3
mobile/devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -1 +1 @@
|
||||
ente - stockage de photos chiffré
|
||||
ente - stockage chiffré des photos
|
||||
@@ -0,0 +1 @@
|
||||
എന്റേ - പൂർണമായും എൻക്രിപ്റ്റ് ചെയ്ത ചിത്രസംഭരണി
|
||||
@@ -1 +1 @@
|
||||
എന്റെ - എൻക്രിപ്റ്റ്ട് ചിത്രസംഭരണി
|
||||
എന്റേ - എൻക്രിപ്റ്റ്ട് ചിത്രസംഭരണി
|
||||
@@ -1,30 +1,30 @@
|
||||
Entre est une application simple qui sauvegarde et organisé vos photos et vidéos.
|
||||
Entre est une application simple qui sauvegarde et organise vos photos et vidéos.
|
||||
|
||||
Si vous recherchez une alternative respectueuse de la vie privée pour préserver vos souvenirs, vous êtes au bon endroit. Avec Ente, ils sont stockés chiffrés de bout-en-bout (e2ee). Cela signifie que vous-seul pouvez les voir.
|
||||
|
||||
Nous avons des applications sur toutes les plateformes, et vos photos seront synchronisées de manière transparente entre tous vos appareils chiffrée de bout en bout (e2ee).
|
||||
Nous avons des applications sur toutes les plateformes, et vos photos seront synchronisées de manière transparente entre tous vos appareils avec un chiffrement de bout en bout (e2ee).
|
||||
|
||||
Ente vous permet également de partager vos albums avec vos proches. Vous pouvez soit les partager directement avec d'autres utilisateurs Ente, chiffrés de bout en bout ou avec des liens visibles publiquement.
|
||||
Ente vous permet également de partager vos albums avec vos proches. Vous pouvez soit les partager directement avec d'autres utilisateurs Ente, chiffrés de bout en bout; ou avec des liens visibles publiquement.
|
||||
|
||||
Vos données chiffrées sont stockées à travers de multiples endroits, dont un abri antiatomique à Paris. Nous prenons la postérité au sérieux et facilitons la conservation de vos souvenirs.
|
||||
|
||||
Nous sommes là pour faire l'application photo la plus sûre de tous les temps, rejoignez-nous !
|
||||
|
||||
CARACTÉRISTIQUES
|
||||
- Sauvegardes de qualité originales, car chaque pixel est important
|
||||
- Abonnement familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille
|
||||
- Sauvegardes de qualité originale, car chaque pixel est important
|
||||
- Abonnements familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille
|
||||
- Dossiers partagés, si vous voulez que votre partenaire profite de vos clichés
|
||||
- Liens ves les albums qui peuvent être protégés par un mot de passe et être configurés pour expirer
|
||||
- Liens vers les albums, qui peuvent être protégés par un mot de passe et être configurés pour expirer
|
||||
- Possibilité de libérer de l'espace en supprimant les fichiers qui ont été sauvegardés en toute sécurité
|
||||
- Éditeur d'images, pour ajouter des touches de finition
|
||||
- Éditeur d'images, pour rajouter des finitions
|
||||
- Favoriser, cacher et revivre vos souvenirs, car ils sont précieux
|
||||
- Importation en un clic les principaux fournisseurs de stockage
|
||||
- Thème sombre, parce que vos photos y sont jolies
|
||||
- Importation en un clic des principaux fournisseurs de stockage
|
||||
- Thème sombre, parce que vos photos y sont plus jolies
|
||||
- 2FA, 3FA, authentification biométrique
|
||||
- et beaucoup de choses encore !
|
||||
|
||||
PRIX
|
||||
Nous ne proposons pas d'abonnement gratuits pour toujours, car il est important pour nous de rester durables et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io.
|
||||
Nous ne proposons pas d'abonnement gratuit à vie, car il est important pour nous de rester durable et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io.
|
||||
|
||||
ASSISTANCE
|
||||
Nous sommes fiers d'offrir un support humain. Si vous êtes un abonné, vous pouvez contacter team@ente.io et vous recevrez une réponse de notre équipe dans les 24 heures.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Stockage de photos chiffré
|
||||
Stockage chiffré des photos
|
||||
|
||||
@@ -1 +1 @@
|
||||
എന്റെ ചിത്രസംഭരണി
|
||||
എന്റേ ചിത്രസംഭരണി
|
||||
|
||||
@@ -1 +1 @@
|
||||
Stockage de photos chiffrées - sauvegardez, organisez et partagez vos photos et vidéos
|
||||
Stockage chiffré des photos - sauvegardez, organisez et partagez vos photos et vidéos
|
||||
@@ -1 +1 @@
|
||||
എന്റെ ചിത്രസംഭരണി
|
||||
എന്റേ ചിത്രസംഭരണി
|
||||
@@ -1,12 +1,12 @@
|
||||
Ente é um aplicativo simples para copiar com segurança automaticamente e organizar suas fotos e vídeos.
|
||||
Ente é um aplicativo básico para salvar e organizar suas fotos e vídeos com segurança.
|
||||
|
||||
Se você esteve procurando por uma alternativa amigável à privacidade para preservar suas memórias, você veio no lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
|
||||
Se você esteve procurando uma alternativa de ótima privacidade para preservar suas memórias, você chegou ao lugar certo. Com Ente, suas fotos e vídeos são criptografados de ponta a ponta (e2ee). Significando que somente você pode vê-las.
|
||||
|
||||
Nós rmos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos vão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).
|
||||
Temos sites/aplicativos para Android, iOS e Computador, e suas fotos serão sincronizadas sem parar em todos os dispositivos numa maneira de criptografia de ponta a ponta (e2ee).
|
||||
|
||||
Ente também torna simples compartilhar seus álbuns com seus entes queridos. Você pode compartilhá-los diretamente com outros usuários do Ente, criptografados de ponta a ponta; ou com links publicamente visíveis.
|
||||
Ente facilita o compartilhamento de álbuns entre entes queridos. Você pode compartilhá-los diretamente com outros usuários Ente, criptografados de ponta a ponta; ou links disponíveis publicamente.
|
||||
|
||||
Seus dados criptografados são replicados em locais diferentes, incluindo um abrigo avançado em Paris. Nós levamos a sério a nossa postura e fazemos que seja fácil garantir que suas memórias vivam.
|
||||
Seus dados criptografados são armazenados em vários locais, incluindo até um refúgio avançado em Paris. Nós levamos nossa postura a sério e facilitamos que você se certifique que suas memórias vivam.
|
||||
|
||||
Estamos aqui para se tornar o aplicativo de fotos mais seguro de todos, venha e participe de nossa jornada!
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
Armazenamento de fotos criptografado - copie com segurança, organize e compartilhe suas fotos e vídeos
|
||||
Armazenamento criptografado de fotos - salve com segurança, organize e compartilhe suas fotos e vídeos
|
||||
@@ -19,7 +19,7 @@ Suntem aici pentru a crea cea mai sigură aplicație de fotografii, veniți ală
|
||||
- Editor de imagini, pentru a adăuga ultimele retușuri
|
||||
- Evidențiați, ascundeți și retrăiți-vă amintirile, căci sunt prețioase
|
||||
- Import cu o singură apăsare din Google, Apple, hard disk și multe altele
|
||||
- Temă întunecată, deoarece fotografiile dvs. arată cu ea
|
||||
- Temă întunecată, deoarece fotografiile dvs. arată bine și cu ea
|
||||
- 2FA, 3FA, autentificare biometrică
|
||||
- și MULTE altele!
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ const supportEmail = 'support@ente.io';
|
||||
const multipartPartSize = 20 * 1024 * 1024;
|
||||
|
||||
const kDefaultProductionEndpoint = 'https://api.ente.io';
|
||||
const kAccountsUrl = 'https://accounts.ente.io';
|
||||
const kCasUrl = 'https://cas.ente.io';
|
||||
const kFamilyUrl = 'https://family.ente.io';
|
||||
|
||||
const int intMaxValue = 9223372036854775807;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photos/core/error-reporting/tunneled_transport.dart';
|
||||
import "package:photos/core/errors.dart";
|
||||
import 'package:photos/models/typedefs.dart';
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -198,6 +199,12 @@ class SuperLogging {
|
||||
$.info("sentry uploader started");
|
||||
}
|
||||
|
||||
unawaited(
|
||||
getDeviceName().then((name) {
|
||||
$.info("Device name: $name");
|
||||
}),
|
||||
);
|
||||
|
||||
if (appConfig.body == null) return;
|
||||
|
||||
if (enable && sentryIsEnabled) {
|
||||
|
||||
557
mobile/lib/emergency/emergency_page.dart
Normal file
@@ -0,0 +1,557 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter_svg/flutter_svg.dart";
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/emergency/emergency_service.dart";
|
||||
import "package:photos/emergency/model.dart";
|
||||
import "package:photos/emergency/other_contact_page.dart";
|
||||
import "package:photos/emergency/select_contact_page.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/components/action_sheet_widget.dart";
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import 'package:photos/ui/components/captioned_text_widget.dart';
|
||||
import 'package:photos/ui/components/divider_widget.dart';
|
||||
import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart';
|
||||
import 'package:photos/ui/components/menu_section_title.dart';
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/ui/components/notification_widget.dart";
|
||||
import 'package:photos/ui/components/title_bar_title_widget.dart';
|
||||
import 'package:photos/ui/components/title_bar_widget.dart';
|
||||
import "package:photos/ui/sharing/user_avator_widget.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/toast_util.dart";
|
||||
|
||||
class EmergencyPage extends StatefulWidget {
|
||||
const EmergencyPage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EmergencyPage> createState() => _EmergencyPageState();
|
||||
}
|
||||
|
||||
class _EmergencyPageState extends State<EmergencyPage> {
|
||||
late int currentUserID;
|
||||
EmergencyInfo? info;
|
||||
|
||||
bool hasTrustedContact = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentUserID = Configuration.instance.getUserID()!;
|
||||
// set info to null after 5 second
|
||||
Future.delayed(
|
||||
const Duration(seconds: 0),
|
||||
() async {
|
||||
unawaited(_fetchData());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
try {
|
||||
final result = await EmergencyContactService.instance.getInfo();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
info = result;
|
||||
if (info != null) {
|
||||
hasTrustedContact = info!.contacts.isNotEmpty;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
showShortToast(
|
||||
context,
|
||||
S.of(context).somethingWentWrong,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final currentUserID = Configuration.instance.getUserID()!;
|
||||
final List<EmergencyContact> othersTrustedContacts =
|
||||
info?.othersEmergencyContact ?? [];
|
||||
final List<EmergencyContact> trustedContacts = info?.contacts ?? [];
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
primary: false,
|
||||
slivers: <Widget>[
|
||||
TitleBarWidget(
|
||||
flexibleSpaceTitle: TitleBarTitleWidget(
|
||||
title: S.of(context).legacy,
|
||||
),
|
||||
),
|
||||
if (info == null)
|
||||
const SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: EnteLoadingWidget(),
|
||||
),
|
||||
),
|
||||
if (info != null)
|
||||
if (info!.recoverSessions.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 20,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: NotificationWidget(
|
||||
startIcon: Icons.warning_amber_rounded,
|
||||
text: context.l10n.recoveryWarning,
|
||||
actionIcon: null,
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
final RecoverySessions recoverSession =
|
||||
info!.recoverSessions[index - 1];
|
||||
return MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: recoverSession.emergencyContact.email,
|
||||
makeTextBold: recoverSession.status.isNotEmpty,
|
||||
textColor: colorScheme.warning500,
|
||||
),
|
||||
leadingIconWidget: UserAvatarWidget(
|
||||
recoverSession.emergencyContact,
|
||||
currentUserID: currentUserID,
|
||||
),
|
||||
leadingIconSize: 24,
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
singleBorderRadius: 8,
|
||||
trailingIcon: Icons.chevron_right,
|
||||
onTap: () async {
|
||||
await showRejectRecoveryDialog(recoverSession);
|
||||
},
|
||||
);
|
||||
},
|
||||
childCount: 1 + info!.recoverSessions.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (info != null)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 8,
|
||||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == 0 && trustedContacts.isNotEmpty) {
|
||||
return MenuSectionTitle(
|
||||
title: S.of(context).trustedContacts,
|
||||
);
|
||||
} else if (index > 0 && index <= trustedContacts.length) {
|
||||
final listIndex = index - 1;
|
||||
final contact = trustedContacts[listIndex];
|
||||
return Column(
|
||||
children: [
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: contact.emergencyContact.email,
|
||||
subTitle: contact.isPendingInvite() ? "⚠" : null,
|
||||
makeTextBold: contact.isPendingInvite(),
|
||||
),
|
||||
leadingIconSize: 24.0,
|
||||
surfaceExecutionStates: false,
|
||||
alwaysShowSuccessState: false,
|
||||
leadingIconWidget: UserAvatarWidget(
|
||||
contact.emergencyContact,
|
||||
type: AvatarType.mini,
|
||||
currentUserID: currentUserID,
|
||||
),
|
||||
menuItemColor:
|
||||
getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
await showRevokeOrRemoveDialog(context, contact);
|
||||
},
|
||||
isTopBorderRadiusRemoved: listIndex > 0,
|
||||
isBottomBorderRadiusRemoved: true,
|
||||
singleBorderRadius: 8,
|
||||
),
|
||||
DividerWidget(
|
||||
dividerType: DividerType.menu,
|
||||
bgColor: getEnteColorScheme(context).fillFaint,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (index == (1 + trustedContacts.length)) {
|
||||
if (trustedContacts.isEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
context.l10n.legacyPageDesc,
|
||||
style: getEnteTextTheme(context).body,
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
width: 200,
|
||||
child: SvgPicture.asset(
|
||||
getEnteColorScheme(context).backdropBase ==
|
||||
backgroundBaseDark
|
||||
? "assets/icons/legacy-light.svg"
|
||||
: "assets/icons/legacy-dark.svg",
|
||||
width: 156,
|
||||
height: 152,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.l10n.legacyPageDesc2,
|
||||
style: getEnteTextTheme(context).smallMuted,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: S.of(context).addTrustedContact,
|
||||
shouldSurfaceExecutionStates: false,
|
||||
onTap: () async {
|
||||
await routeToPage(
|
||||
context,
|
||||
AddContactPage(info!),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
unawaited(_fetchData());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: trustedContacts.isNotEmpty
|
||||
? S.of(context).addMore
|
||||
: S.of(context).addTrustedContact,
|
||||
makeTextBold: true,
|
||||
),
|
||||
leadingIcon: Icons.add_outlined,
|
||||
surfaceExecutionStates: false,
|
||||
menuItemColor: getEnteColorScheme(context).fillFaint,
|
||||
onTap: () async {
|
||||
await routeToPage(
|
||||
context,
|
||||
AddContactPage(info!),
|
||||
forceCustomPageRoute: true,
|
||||
);
|
||||
unawaited(_fetchData());
|
||||
},
|
||||
isTopBorderRadiusRemoved: trustedContacts.isNotEmpty,
|
||||
singleBorderRadius: 8,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
childCount: 1 + trustedContacts.length + 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (info != null && info!.othersEmergencyContact.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(top: 0, left: 16, right: 16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == 0 && (othersTrustedContacts.isNotEmpty)) {
|
||||
return Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: DividerWidget(
|
||||
dividerType: DividerType.solid,
|
||||
),
|
||||
),
|
||||
MenuSectionTitle(
|
||||
title: context.l10n.legacyAccounts,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (index > 0 &&
|
||||
index <= othersTrustedContacts.length) {
|
||||
final listIndex = index - 1;
|
||||
final currentUser = othersTrustedContacts[listIndex];
|
||||
final isLastItem = index == othersTrustedContacts.length;
|
||||
return Column(
|
||||
children: [
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: currentUser.user.email,
|
||||
makeTextBold: currentUser.isPendingInvite(),
|
||||
subTitle:
|
||||
currentUser.isPendingInvite() ? "⚠" : null,
|
||||
),
|
||||
leadingIconSize: 24.0,
|
||||
leadingIconWidget: UserAvatarWidget(
|
||||
currentUser.user,
|
||||
type: AvatarType.mini,
|
||||
currentUserID: currentUserID,
|
||||
),
|
||||
menuItemColor:
|
||||
getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
if (currentUser.isPendingInvite()) {
|
||||
await showAcceptOrDeclineDialog(
|
||||
context,
|
||||
currentUser,
|
||||
);
|
||||
} else {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return OtherContactPage(
|
||||
contact: currentUser,
|
||||
emergencyInfo: info!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// await routeToPage(
|
||||
// context,
|
||||
// OtherContactPage(
|
||||
// contact: currentUser,
|
||||
// emergencyInfo: info!,
|
||||
// ),
|
||||
// );
|
||||
if (mounted) {
|
||||
unawaited(_fetchData());
|
||||
}
|
||||
}
|
||||
},
|
||||
isTopBorderRadiusRemoved: listIndex > 0,
|
||||
isBottomBorderRadiusRemoved: !isLastItem,
|
||||
singleBorderRadius: 8,
|
||||
surfaceExecutionStates: false,
|
||||
),
|
||||
isLastItem
|
||||
? const SizedBox.shrink()
|
||||
: DividerWidget(
|
||||
dividerType: DividerType.menu,
|
||||
bgColor:
|
||||
getEnteColorScheme(context).fillFaint,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
childCount: 1 + othersTrustedContacts.length + 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showRevokeOrRemoveDialog(
|
||||
BuildContext context,
|
||||
EmergencyContact contact,
|
||||
) async {
|
||||
if (contact.isPendingInvite()) {
|
||||
await showActionSheet(
|
||||
context: context,
|
||||
body:
|
||||
"You have invited ${contact.emergencyContact.email} to be a trusted contact",
|
||||
bodyHighlight: "They are yet to accept your invite",
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).removeInvite,
|
||||
buttonType: ButtonType.critical,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.first,
|
||||
shouldStickToDarkTheme: true,
|
||||
shouldSurfaceExecutionStates: true,
|
||||
shouldShowSuccessConfirmation: false,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance
|
||||
.updateContact(contact, ContactState.userRevokedContact);
|
||||
info?.contacts.remove(contact);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
unawaited(_fetchData());
|
||||
}
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).cancel,
|
||||
buttonType: ButtonType.tertiary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldStickToDarkTheme: true,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
await showActionSheet(
|
||||
context: context,
|
||||
body:
|
||||
"You have added ${contact.emergencyContact.email} as a trusted contact",
|
||||
bodyHighlight: "They have accepted your invite",
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).remove,
|
||||
buttonType: ButtonType.critical,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldStickToDarkTheme: true,
|
||||
shouldSurfaceExecutionStates: true,
|
||||
shouldShowSuccessConfirmation: false,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance
|
||||
.updateContact(contact, ContactState.userRevokedContact);
|
||||
info?.contacts.remove(contact);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
unawaited(_fetchData());
|
||||
}
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).cancel,
|
||||
buttonType: ButtonType.tertiary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.third,
|
||||
shouldStickToDarkTheme: true,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showAcceptOrDeclineDialog(
|
||||
BuildContext context,
|
||||
EmergencyContact contact,
|
||||
) async {
|
||||
await showActionSheet(
|
||||
context: context,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).acceptTrustInvite,
|
||||
buttonType: ButtonType.primary,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonAction: ButtonAction.first,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance
|
||||
.updateContact(contact, ContactState.contactAccepted);
|
||||
final updatedContact =
|
||||
contact.copyWith(state: ContactState.contactAccepted);
|
||||
info?.othersEmergencyContact.remove(contact);
|
||||
info?.othersEmergencyContact.add(updatedContact);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).declineTrustInvite,
|
||||
buttonType: ButtonType.critical,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldStickToDarkTheme: true,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance
|
||||
.updateContact(contact, ContactState.contactDenied);
|
||||
info?.othersEmergencyContact.remove(contact);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).cancel,
|
||||
buttonType: ButtonType.tertiary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.third,
|
||||
shouldStickToDarkTheme: true,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
body: S.of(context).legacyInvite(contact.user.email),
|
||||
actionSheetType: ActionSheetType.defaultActionSheet,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> showRejectRecoveryDialog(RecoverySessions session) async {
|
||||
final String emergencyContactEmail = session.emergencyContact.email;
|
||||
await showActionSheet(
|
||||
context: context,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
labelText: context.l10n.rejectRecovery,
|
||||
buttonSize: ButtonSize.large,
|
||||
shouldStickToDarkTheme: true,
|
||||
buttonType: ButtonType.critical,
|
||||
buttonAction: ButtonAction.first,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance.rejectRecovery(session);
|
||||
info?.recoverSessions
|
||||
.removeWhere((element) => element.id == session.id);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
unawaited(_fetchData());
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
if (kDebugMode)
|
||||
ButtonWidget(
|
||||
labelText: "Approve recovery (to be removed)",
|
||||
buttonType: ButtonType.primary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.second,
|
||||
shouldStickToDarkTheme: true,
|
||||
onTap: () async {
|
||||
await EmergencyContactService.instance.approveRecovery(session);
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
unawaited(_fetchData());
|
||||
},
|
||||
isInAlert: true,
|
||||
),
|
||||
ButtonWidget(
|
||||
labelText: S.of(context).cancel,
|
||||
buttonType: ButtonType.tertiary,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.third,
|
||||
shouldStickToDarkTheme: true,
|
||||
isInAlert: true,
|
||||
),
|
||||
],
|
||||
body: context.l10n.recoveryWarningBody(emergencyContactEmail),
|
||||
actionSheetType: ActionSheetType.defaultActionSheet,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
275
mobile/lib/emergency/emergency_service.dart
Normal file
@@ -0,0 +1,275 @@
|
||||
import "dart:convert";
|
||||
import "dart:math";
|
||||
import "dart:typed_data";
|
||||
|
||||
import "package:dio/dio.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/emergency/model.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/models/api/user/srp.dart";
|
||||
import "package:photos/models/key_attributes.dart";
|
||||
import "package:photos/models/set_keys_request.dart";
|
||||
import "package:photos/services/user_service.dart";
|
||||
import "package:photos/ui/common/user_dialogs.dart";
|
||||
import "package:photos/utils/crypto_util.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/email_util.dart";
|
||||
import "package:pointycastle/pointycastle.dart";
|
||||
import "package:pointycastle/random/fortuna_random.dart";
|
||||
import "package:pointycastle/srp/srp6_client.dart";
|
||||
import "package:pointycastle/srp/srp6_standard_groups.dart";
|
||||
import "package:pointycastle/srp/srp6_util.dart";
|
||||
import "package:pointycastle/srp/srp6_verifier_generator.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
|
||||
class EmergencyContactService {
|
||||
late Dio _enteDio;
|
||||
late UserService _userService;
|
||||
late Configuration _config;
|
||||
late final Logger _logger = Logger("EmergencyContactService");
|
||||
|
||||
EmergencyContactService._privateConstructor() {
|
||||
_enteDio = NetworkClient.instance.enteDio;
|
||||
_userService = UserService.instance;
|
||||
_config = Configuration.instance;
|
||||
}
|
||||
|
||||
static final EmergencyContactService instance =
|
||||
EmergencyContactService._privateConstructor();
|
||||
|
||||
Future<bool> addContact(BuildContext context, String email) async {
|
||||
if (!isValidEmail(email)) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
S.of(context).invalidEmailAddress,
|
||||
S.of(context).enterValidEmail,
|
||||
);
|
||||
return false;
|
||||
} else if (email.trim() == Configuration.instance.getEmail()) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
S.of(context).oops,
|
||||
S.of(context).youCannotShareWithYourself,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
final String? publicKey = await _userService.getPublicKey(email);
|
||||
if (publicKey == null) {
|
||||
await showInviteDialog(context, email);
|
||||
return false;
|
||||
}
|
||||
final Uint8List recoveryKey = Configuration.instance.getRecoveryKey();
|
||||
final encryptedKey = CryptoUtil.sealSync(
|
||||
recoveryKey,
|
||||
CryptoUtil.base642bin(publicKey),
|
||||
);
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/add",
|
||||
data: {
|
||||
"email": email.trim(),
|
||||
"encryptedKey": CryptoUtil.bin2base64(encryptedKey),
|
||||
},
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<EmergencyInfo> getInfo() async {
|
||||
try {
|
||||
final response = await _enteDio.get("/emergency-contacts/info");
|
||||
return EmergencyInfo.fromJson(response.data);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to get info', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateContact(
|
||||
EmergencyContact contact,
|
||||
ContactState state,
|
||||
) async {
|
||||
try {
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/update",
|
||||
data: {
|
||||
"userID": contact.user.id,
|
||||
"emergencyContactID": contact.emergencyContact.id,
|
||||
"state": state.stringValue,
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to update contact', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startRecovery(EmergencyContact contact) async {
|
||||
try {
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/start-recovery",
|
||||
data: {
|
||||
"userID": contact.user.id,
|
||||
"emergencyContactID": contact.emergencyContact.id,
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to start recovery', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopRecovery(RecoverySessions session) async {
|
||||
try {
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/stop-recovery",
|
||||
data: {
|
||||
"userID": session.user.id,
|
||||
"emergencyContactID": session.emergencyContact.id,
|
||||
"id": session.id,
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> rejectRecovery(RecoverySessions session) async {
|
||||
try {
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/reject-recovery",
|
||||
data: {
|
||||
"userID": session.user.id,
|
||||
"emergencyContactID": session.emergencyContact.id,
|
||||
"id": session.id,
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> approveRecovery(RecoverySessions session) async {
|
||||
try {
|
||||
await _enteDio.post(
|
||||
"/emergency-contacts/approve-recovery",
|
||||
data: {
|
||||
"userID": session.user.id,
|
||||
"emergencyContactID": session.emergencyContact.id,
|
||||
"id": session.id,
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to approve recovery', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<(String, KeyAttributes)> getRecoveryInfo(
|
||||
RecoverySessions sessions,
|
||||
) async {
|
||||
try {
|
||||
final resp = await _enteDio.get(
|
||||
"/emergency-contacts/recovery-info/${sessions.id}",
|
||||
);
|
||||
final String encryptedKey = resp.data["encryptedKey"]!;
|
||||
final decryptedKey = CryptoUtil.openSealSync(
|
||||
CryptoUtil.base642bin(encryptedKey),
|
||||
CryptoUtil.base642bin(_config.getKeyAttributes()!.publicKey),
|
||||
_config.getSecretKey()!,
|
||||
);
|
||||
final String hexRecoveryKey = CryptoUtil.bin2hex(decryptedKey);
|
||||
final KeyAttributes keyAttributes =
|
||||
KeyAttributes.fromMap(resp.data['userKeyAttr']);
|
||||
return (hexRecoveryKey, keyAttributes);
|
||||
} catch (e, s) {
|
||||
Logger("EmergencyContact").severe('failed to stop recovery', e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changePasswordForOther(
|
||||
Uint8List loginKey,
|
||||
SetKeysRequest setKeysRequest,
|
||||
RecoverySessions recoverySessions,
|
||||
) async {
|
||||
try {
|
||||
final SRP6GroupParameters kDefaultSrpGroup =
|
||||
SRP6StandardGroups.rfc5054_4096;
|
||||
final String username = const Uuid().v4().toString();
|
||||
final SecureRandom random = _getSecureRandom();
|
||||
final Uint8List identity = Uint8List.fromList(utf8.encode(username));
|
||||
final Uint8List password = loginKey;
|
||||
final Uint8List salt = random.nextBytes(16);
|
||||
final gen = SRP6VerifierGenerator(
|
||||
group: kDefaultSrpGroup,
|
||||
digest: Digest('SHA-256'),
|
||||
);
|
||||
final v = gen.generateVerifier(salt, identity, password);
|
||||
|
||||
final client = SRP6Client(
|
||||
group: kDefaultSrpGroup,
|
||||
digest: Digest('SHA-256'),
|
||||
random: random,
|
||||
);
|
||||
|
||||
final A = client.generateClientCredentials(salt, identity, password);
|
||||
final request = SetupSRPRequest(
|
||||
srpUserID: username,
|
||||
srpSalt: base64Encode(salt),
|
||||
srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)),
|
||||
srpA: base64Encode(SRP6Util.encodeBigInt(A!)),
|
||||
isUpdate: false,
|
||||
);
|
||||
final response = await _enteDio.post(
|
||||
"/emergency-contacts/init-change-password",
|
||||
data: {
|
||||
"recoveryID": recoverySessions.id,
|
||||
"setupSRPRequest": request.toMap(),
|
||||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final SetupSRPResponse setupSRPResponse =
|
||||
SetupSRPResponse.fromJson(response.data);
|
||||
final serverB =
|
||||
SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB));
|
||||
|
||||
// ignore: unused_local_variable
|
||||
final clientS = client.calculateSecret(serverB);
|
||||
final clientM = client.calculateClientEvidenceMessage();
|
||||
// ignore: unused_local_variable
|
||||
late Response srpCompleteResponse;
|
||||
srpCompleteResponse = await _enteDio.post(
|
||||
"/emergency-contacts/change-password",
|
||||
data: {
|
||||
"recoveryID": recoverySessions.id,
|
||||
'updateSrpAndKeysRequest': {
|
||||
'setupID': setupSRPResponse.setupID,
|
||||
'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)),
|
||||
'updatedKeyAttr': setKeysRequest.toMap(),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw Exception("register-srp action failed");
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to change password for other", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
SecureRandom _getSecureRandom() {
|
||||
final List<int> seeds = [];
|
||||
final random = Random.secure();
|
||||
for (int i = 0; i < 32; i++) {
|
||||
seeds.add(random.nextInt(255));
|
||||
}
|
||||
final secureRandom = FortunaRandom();
|
||||
secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
|
||||
return secureRandom;
|
||||
}
|
||||
}
|
||||
153
mobile/lib/emergency/model.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
|
||||
enum ContactState {
|
||||
userInvitedContact,
|
||||
userRevokedContact,
|
||||
contactAccepted,
|
||||
contactLeft,
|
||||
contactDenied,
|
||||
unknown,
|
||||
}
|
||||
|
||||
extension ContactStateExtension on ContactState {
|
||||
String get stringValue {
|
||||
switch (this) {
|
||||
case ContactState.userInvitedContact:
|
||||
return "INVITED";
|
||||
case ContactState.userRevokedContact:
|
||||
return "REVOKED";
|
||||
case ContactState.contactAccepted:
|
||||
return "ACCEPTED";
|
||||
case ContactState.contactLeft:
|
||||
return "CONTACT_LEFT";
|
||||
case ContactState.contactDenied:
|
||||
return "CONTACT_DENIED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static ContactState fromString(String value) {
|
||||
switch (value) {
|
||||
case "INVITED":
|
||||
return ContactState.userInvitedContact;
|
||||
case "REVOKED":
|
||||
return ContactState.userRevokedContact;
|
||||
case "ACCEPTED":
|
||||
return ContactState.contactAccepted;
|
||||
case "CONTACT_LEFT":
|
||||
return ContactState.contactLeft;
|
||||
case "CONTACT_DENIED":
|
||||
return ContactState.contactDenied;
|
||||
default:
|
||||
return ContactState.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EmergencyContact {
|
||||
final User user;
|
||||
final User emergencyContact;
|
||||
final ContactState state;
|
||||
final int recoveryNoticeInDays;
|
||||
|
||||
EmergencyContact(
|
||||
this.user,
|
||||
this.emergencyContact,
|
||||
this.state,
|
||||
this.recoveryNoticeInDays,
|
||||
);
|
||||
|
||||
// copyWith
|
||||
EmergencyContact copyWith({
|
||||
User? user,
|
||||
User? emergencyContact,
|
||||
ContactState? state,
|
||||
int? recoveryNoticeInDays,
|
||||
}) {
|
||||
return EmergencyContact(
|
||||
user ?? this.user,
|
||||
emergencyContact ?? this.emergencyContact,
|
||||
state ?? this.state,
|
||||
recoveryNoticeInDays ?? this.recoveryNoticeInDays,
|
||||
);
|
||||
}
|
||||
|
||||
// fromJson
|
||||
EmergencyContact.fromJson(Map<String, dynamic> json)
|
||||
: user = User.fromMap(json['user']),
|
||||
emergencyContact = User.fromMap(json['emergencyContact']),
|
||||
state = ContactStateExtension.fromString(json['state'] as String),
|
||||
recoveryNoticeInDays = json['recoveryNoticeInDays'];
|
||||
|
||||
bool isCurrentUserContact(int userID) {
|
||||
return user.id == userID;
|
||||
}
|
||||
|
||||
bool isPendingInvite() {
|
||||
return state == ContactState.userInvitedContact;
|
||||
}
|
||||
}
|
||||
|
||||
class EmergencyInfo {
|
||||
// List of emergency contacts added by the user
|
||||
final List<EmergencyContact> contacts;
|
||||
|
||||
// List of recovery sessions that are created to recover current user account
|
||||
final List<RecoverySessions> recoverSessions;
|
||||
|
||||
// List of emergency contacts that have added current user as their emergency contact
|
||||
final List<EmergencyContact> othersEmergencyContact;
|
||||
|
||||
// List of recovery sessions that are created to recover grantor's account
|
||||
final List<RecoverySessions> othersRecoverySession;
|
||||
|
||||
EmergencyInfo(
|
||||
this.contacts,
|
||||
this.recoverSessions,
|
||||
this.othersEmergencyContact,
|
||||
this.othersRecoverySession,
|
||||
);
|
||||
|
||||
// from json
|
||||
EmergencyInfo.fromJson(Map<String, dynamic> json)
|
||||
: contacts = (json['contacts'] as List)
|
||||
.map((contact) => EmergencyContact.fromJson(contact))
|
||||
.toList(),
|
||||
recoverSessions = (json['recoverSessions'] as List)
|
||||
.map((session) => RecoverySessions.fromJson(session))
|
||||
.toList(),
|
||||
othersEmergencyContact = (json['othersEmergencyContact'] as List)
|
||||
.map((grantor) => EmergencyContact.fromJson(grantor))
|
||||
.toList(),
|
||||
othersRecoverySession = (json['othersRecoverySession'] as List)
|
||||
.map((session) => RecoverySessions.fromJson(session))
|
||||
.toList();
|
||||
}
|
||||
|
||||
class RecoverySessions {
|
||||
final String id;
|
||||
final User user;
|
||||
final User emergencyContact;
|
||||
final String status;
|
||||
final int waitTill;
|
||||
final int createdAt;
|
||||
|
||||
RecoverySessions(
|
||||
this.id,
|
||||
this.user,
|
||||
this.emergencyContact,
|
||||
this.status,
|
||||
this.waitTill,
|
||||
this.createdAt,
|
||||
);
|
||||
|
||||
// fromJson
|
||||
RecoverySessions.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
user = User.fromMap(json['user']),
|
||||
emergencyContact = User.fromMap(json['emergencyContact']),
|
||||
status = json['status'],
|
||||
waitTill = json['waitTill'],
|
||||
createdAt = json['createdAt'];
|
||||
}
|
||||