Compare commits
649 Commits
auth-v3.0.
...
cli-v0.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26cbc5a2f0 | ||
|
|
4674ab63e9 | ||
|
|
641efa15be | ||
|
|
55e2911eef | ||
|
|
d9553fc5bb | ||
|
|
9ce613eae5 | ||
|
|
be3e33f5c5 | ||
|
|
f5fee2185c | ||
|
|
44fefac37c | ||
|
|
8b53dac00c | ||
|
|
abf13245dc | ||
|
|
e87475beb7 | ||
|
|
55b62ce3cc | ||
|
|
b2405e8b59 | ||
|
|
1eaa635d0e | ||
|
|
291d5c437c | ||
|
|
7f4b0c3d10 | ||
|
|
4718e640b4 | ||
|
|
c7c50293df | ||
|
|
b7181963ca | ||
|
|
fa06a15ad7 | ||
|
|
133693d058 | ||
|
|
d3ad6cbd4e | ||
|
|
bfe34a908c | ||
|
|
f6bdeef33d | ||
|
|
9a7ba8a406 | ||
|
|
a850500beb | ||
|
|
72a3f7f17a | ||
|
|
c8d30323e4 | ||
|
|
029872e54e | ||
|
|
3ad8f73289 | ||
|
|
2ad4912d7e | ||
|
|
b3c907f8ee | ||
|
|
50a8ddc002 | ||
|
|
5fc03bca1c | ||
|
|
05e4d18a14 | ||
|
|
387ca79b6d | ||
|
|
ddaa872b97 | ||
|
|
67169b4efa | ||
|
|
52b3a6d0f7 | ||
|
|
575c5aad81 | ||
|
|
f062074177 | ||
|
|
41124d07a5 | ||
|
|
5042e3cbd7 | ||
|
|
1227bbc4a9 | ||
|
|
27a5aa99c0 | ||
|
|
5049b5cc4e | ||
|
|
beedbd0991 | ||
|
|
113a949a4b | ||
|
|
c70c498d38 | ||
|
|
c0c4412b19 | ||
|
|
84ac002885 | ||
|
|
29f89ab901 | ||
|
|
253b74d58f | ||
|
|
89064f77ae | ||
|
|
9d309dd6de | ||
|
|
9fbe02eeac | ||
|
|
0d38c6ac1b | ||
|
|
453f196a63 | ||
|
|
f14f973a61 | ||
|
|
a830e42ead | ||
|
|
62e5950429 | ||
|
|
1c241d70fd | ||
|
|
81472fdafb | ||
|
|
c1097de27f | ||
|
|
f647355666 | ||
|
|
f871255833 | ||
|
|
ae9d406fe9 | ||
|
|
8f7af989bb | ||
|
|
21567d546e | ||
|
|
400a6a9054 | ||
|
|
7cc29c302e | ||
|
|
654f6b8934 | ||
|
|
3e1dbce629 | ||
|
|
ce93ce6529 | ||
|
|
e682b065d1 | ||
|
|
9f2d770bc2 | ||
|
|
27523e2f10 | ||
|
|
966e5527ec | ||
|
|
074e867886 | ||
|
|
46761622f1 | ||
|
|
45d7e3da2c | ||
|
|
1f6be04bf4 | ||
|
|
6327a7f9da | ||
|
|
9dbec2729c | ||
|
|
2b3b84de0f | ||
|
|
a6a0a24b26 | ||
|
|
1a292aae27 | ||
|
|
154cb7a8bc | ||
|
|
bb9a605b0d | ||
|
|
b2277cfcc2 | ||
|
|
69c18cb852 | ||
|
|
b9a07e536c | ||
|
|
6b0501e272 | ||
|
|
57404e1f49 | ||
|
|
b5c52a4ae2 | ||
|
|
2abcb709d9 | ||
|
|
13d15ceeb9 | ||
|
|
34166ecffb | ||
|
|
40d35e157e | ||
|
|
91be44c4c5 | ||
|
|
6d3391528d | ||
|
|
4b202d2dda | ||
|
|
ac8677d7b4 | ||
|
|
227873cc2d | ||
|
|
3fc41aecca | ||
|
|
6dc26b9124 | ||
|
|
841a67443d | ||
|
|
c71e56ec43 | ||
|
|
fd4a788953 | ||
|
|
81f9efbace | ||
|
|
23c73a83eb | ||
|
|
6e6c88826e | ||
|
|
80be753d77 | ||
|
|
e41e0eadee | ||
|
|
53dea9dcf3 | ||
|
|
6b1484671b | ||
|
|
c3347bae5d | ||
|
|
b17933a2b3 | ||
|
|
d448676b8f | ||
|
|
c8a7152cdc | ||
|
|
3c3f9b2b48 | ||
|
|
f66170b5b2 | ||
|
|
8e1d7bc884 | ||
|
|
dafdeca7e4 | ||
|
|
6be42225c2 | ||
|
|
403cc3cca0 | ||
|
|
321422e915 | ||
|
|
3c92349054 | ||
|
|
c3f6ecbf6a | ||
|
|
35090a6cdd | ||
|
|
ab61fee8de | ||
|
|
896de62794 | ||
|
|
d9200f4703 | ||
|
|
3c0d82279c | ||
|
|
f6bd99386e | ||
|
|
85785f7543 | ||
|
|
aa353b57e8 | ||
|
|
cbdd82f6c0 | ||
|
|
ddddc09226 | ||
|
|
bae4c65ab3 | ||
|
|
54654159ff | ||
|
|
8a1acc756e | ||
|
|
61fb9cf544 | ||
|
|
7739be4e21 | ||
|
|
3b8ab89647 | ||
|
|
4ce02fba93 | ||
|
|
72851397b1 | ||
|
|
08a073fc1b | ||
|
|
ce1ba6112f | ||
|
|
6097f9d4ba | ||
|
|
daf72d8ac6 | ||
|
|
9adc8126bb | ||
|
|
c968cc3c41 | ||
|
|
7f150d8dc7 | ||
|
|
f8aa749799 | ||
|
|
2fb7ee0171 | ||
|
|
a44e932c84 | ||
|
|
d92e7e0c5d | ||
|
|
9ae13ec159 | ||
|
|
d83eedc93d | ||
|
|
588df2c346 | ||
|
|
1e792459a1 | ||
|
|
245e9c0fff | ||
|
|
85ce2d7e49 | ||
|
|
ee3ea77831 | ||
|
|
9922b704e8 | ||
|
|
a24cfe9d05 | ||
|
|
6c77901396 | ||
|
|
272025e657 | ||
|
|
798f5d2e11 | ||
|
|
6b655c8157 | ||
|
|
2ba802d59f | ||
|
|
27d89c4952 | ||
|
|
642ea88319 | ||
|
|
979730d740 | ||
|
|
079ff43557 | ||
|
|
b3a0bc624b | ||
|
|
cee093c214 | ||
|
|
8dd0d58319 | ||
|
|
34d4aeaf56 | ||
|
|
431cd39358 | ||
|
|
bb46e98e85 | ||
|
|
f0f3af96d1 | ||
|
|
9c60fe6f3f | ||
|
|
0cae667b44 | ||
|
|
2f7d1401cd | ||
|
|
cfb4ded991 | ||
|
|
b1e64cadf6 | ||
|
|
4aaafd3b08 | ||
|
|
f34a4d4a21 | ||
|
|
c8a3728f5d | ||
|
|
b10f4ee18a | ||
|
|
433c23ca07 | ||
|
|
cb0cffce3d | ||
|
|
853f291de3 | ||
|
|
9887d44416 | ||
|
|
ca7b609217 | ||
|
|
f1b2e2bec2 | ||
|
|
f5947a0c4a | ||
|
|
126727a9cc | ||
|
|
5e49b8a528 | ||
|
|
3664532f91 | ||
|
|
8ea7a742b1 | ||
|
|
77f3503a0b | ||
|
|
8975af7a71 | ||
|
|
b64077d5e7 | ||
|
|
50968fd6a1 | ||
|
|
89a47026d9 | ||
|
|
9a8c4d9cfd | ||
|
|
284bca782e | ||
|
|
1535f61653 | ||
|
|
b45dfa9cfc | ||
|
|
f9b3a931a5 | ||
|
|
dd83edf0e3 | ||
|
|
6988b70d9f | ||
|
|
d33c92a51c | ||
|
|
54aecfd721 | ||
|
|
d8f3a48a6f | ||
|
|
cb8d572951 | ||
|
|
c1e5249c9b | ||
|
|
07552f7a89 | ||
|
|
6c28dede44 | ||
|
|
b2df698e42 | ||
|
|
2ae869075e | ||
|
|
95ae7a6cd0 | ||
|
|
d1b2d5696a | ||
|
|
705fae35e6 | ||
|
|
cad07cd96f | ||
|
|
f66ac40903 | ||
|
|
07dc0231ee | ||
|
|
40db48b88f | ||
|
|
50556b9930 | ||
|
|
321ae0b7fc | ||
|
|
9b0b7f11f1 | ||
|
|
3415739f43 | ||
|
|
855e706f4b | ||
|
|
eaaa26c2e3 | ||
|
|
0f502eb9c2 | ||
|
|
af571669da | ||
|
|
69e2a36933 | ||
|
|
bed57eb03e | ||
|
|
03bc8f0493 | ||
|
|
3d122b9f9d | ||
|
|
e90eb50a50 | ||
|
|
5e18ae1938 | ||
|
|
37d3776e28 | ||
|
|
05579ef368 | ||
|
|
90e467c7c0 | ||
|
|
ae61fc9c6f | ||
|
|
c291fa70d3 | ||
|
|
99cf23d286 | ||
|
|
d854d5820e | ||
|
|
232acfa211 | ||
|
|
f25f119ca1 | ||
|
|
89a61b3bf7 | ||
|
|
380d37267b | ||
|
|
9cf5691e42 | ||
|
|
8f474a4500 | ||
|
|
c7be2270ff | ||
|
|
ced1f0bd79 | ||
|
|
9f58f1eeb3 | ||
|
|
04be2b6a2c | ||
|
|
9f361237b1 | ||
|
|
8cb7cae7b7 | ||
|
|
a2a209a849 | ||
|
|
d413c4f4c1 | ||
|
|
ee8976e92b | ||
|
|
baa90c42ad | ||
|
|
30ade541df | ||
|
|
86fb8ebfaf | ||
|
|
b2e8c3c0eb | ||
|
|
e203a8378e | ||
|
|
b100f1d4bf | ||
|
|
7b4559f3ca | ||
|
|
eac142025d | ||
|
|
c5aa536c3b | ||
|
|
05406333e4 | ||
|
|
8ebd50606a | ||
|
|
cbcfc243fc | ||
|
|
7d497b5ae1 | ||
|
|
b28f6c3d8c | ||
|
|
71a8049a35 | ||
|
|
e95cba0ace | ||
|
|
e836ada0d6 | ||
|
|
19a104374d | ||
|
|
693ef45e2c | ||
|
|
55bdb070ce | ||
|
|
27127ff3d4 | ||
|
|
345c706814 | ||
|
|
49133b7b86 | ||
|
|
3a5311cdcc | ||
|
|
7182795732 | ||
|
|
ca00b3b558 | ||
|
|
4bcb765810 | ||
|
|
17b49595a0 | ||
|
|
b99c573d3a | ||
|
|
d3d3e4dbed | ||
|
|
ba1af5eaf0 | ||
|
|
14cf59c1e5 | ||
|
|
452872156a | ||
|
|
4f31bd625d | ||
|
|
6bf6f78147 | ||
|
|
5576f99548 | ||
|
|
5bbe768acb | ||
|
|
babe378301 | ||
|
|
b2fda16561 | ||
|
|
6d289d73db | ||
|
|
17acf4b3ee | ||
|
|
4d666d4b01 | ||
|
|
619f8319ed | ||
|
|
3261da3515 | ||
|
|
d0d491f7f5 | ||
|
|
db3764d448 | ||
|
|
5fe5451f5c | ||
|
|
6d3d5d03f8 | ||
|
|
582eb9e1ea | ||
|
|
51770a11ef | ||
|
|
1ea7a8f3a7 | ||
|
|
b4536a7aee | ||
|
|
9d2be29fad | ||
|
|
f92a18efca | ||
|
|
af382d483d | ||
|
|
99f1ba799d | ||
|
|
1548bcd378 | ||
|
|
c2fc0a3d57 | ||
|
|
39a706ea20 | ||
|
|
38d6464f55 | ||
|
|
c5b6297cea | ||
|
|
390b4b1f81 | ||
|
|
b19b34b3dc | ||
|
|
5690d613bb | ||
|
|
bb713cfc76 | ||
|
|
4a0c93373d | ||
|
|
b42759d473 | ||
|
|
2e93281368 | ||
|
|
c18be32c09 | ||
|
|
650163c341 | ||
|
|
d101208baa | ||
|
|
76f7215269 | ||
|
|
621c482529 | ||
|
|
314c8f69f2 | ||
|
|
1f45cf00c7 | ||
|
|
e0e80ee91f | ||
|
|
225278adb7 | ||
|
|
8d30bfbefa | ||
|
|
ad96f679c9 | ||
|
|
4b896d3aab | ||
|
|
533e6d06e7 | ||
|
|
e88b5c99ba | ||
|
|
1ec7e02695 | ||
|
|
19e08cf803 | ||
|
|
08073b927c | ||
|
|
711a44412d | ||
|
|
c9f94f062b | ||
|
|
c8205b8475 | ||
|
|
b0d3fcfe79 | ||
|
|
11a354c560 | ||
|
|
823f739c32 | ||
|
|
f8876c8154 | ||
|
|
90db45d845 | ||
|
|
6a1f5945b9 | ||
|
|
f7ca838428 | ||
|
|
2b065dd68d | ||
|
|
f168ea9e1e | ||
|
|
58702103f3 | ||
|
|
dfb3a6f65c | ||
|
|
491f38b120 | ||
|
|
79c0880c9c | ||
|
|
834b8f78b7 | ||
|
|
cbf0336cd0 | ||
|
|
431d629641 | ||
|
|
94c1cc011b | ||
|
|
b26b0759d6 | ||
|
|
d51fb99fd3 | ||
|
|
0379216e05 | ||
|
|
ccd486f659 | ||
|
|
ce3ab55069 | ||
|
|
34effef810 | ||
|
|
56aceb589d | ||
|
|
92a2506f8a | ||
|
|
e23bc2602f | ||
|
|
69beecb7bb | ||
|
|
880b13f436 | ||
|
|
9061caac99 | ||
|
|
11cc8e46b7 | ||
|
|
54820689c2 | ||
|
|
acebb86fec | ||
|
|
367e09599d | ||
|
|
b9fe509567 | ||
|
|
82bffd81de | ||
|
|
7340443b86 | ||
|
|
2cd1dfd720 | ||
|
|
3c8d29bcdc | ||
|
|
7a6fa1cd80 | ||
|
|
06a698ddbb | ||
|
|
3b8c48e92d | ||
|
|
3c0cb20a9b | ||
|
|
74bb169f0d | ||
|
|
302890baef | ||
|
|
54e33d3f42 | ||
|
|
0adb94f405 | ||
|
|
7d634aa703 | ||
|
|
b1e0c83733 | ||
|
|
d4af7792d4 | ||
|
|
f301ab57f2 | ||
|
|
7b0f5909b5 | ||
|
|
e9064f6904 | ||
|
|
022448155d | ||
|
|
ed830dc387 | ||
|
|
0d21fc77b5 | ||
|
|
b26c6e9c0d | ||
|
|
a79d11c263 | ||
|
|
a470ed4dfa | ||
|
|
500d7da306 | ||
|
|
057d11f39b | ||
|
|
c9de6d7a82 | ||
|
|
698ac9f29e | ||
|
|
637adb4617 | ||
|
|
a0d26c860c | ||
|
|
bd2444d353 | ||
|
|
ca24a86179 | ||
|
|
fffe96a4c7 | ||
|
|
0ec75c2435 | ||
|
|
cb78c848d6 | ||
|
|
6594db9393 | ||
|
|
f6c40ee67d | ||
|
|
36aa33ed5a | ||
|
|
776dba4fb0 | ||
|
|
7f49f530c5 | ||
|
|
ef6fe80944 | ||
|
|
370b28f9e4 | ||
|
|
05e737cb11 | ||
|
|
0fdb58eda1 | ||
|
|
1ce90839fe | ||
|
|
697946f415 | ||
|
|
cc91cb8012 | ||
|
|
754de7065f | ||
|
|
5587373b42 | ||
|
|
f1d1a4a9e1 | ||
|
|
dc38a8bc9f | ||
|
|
edf9f743f4 | ||
|
|
fec040e528 | ||
|
|
86f96a5713 | ||
|
|
c3fb472287 | ||
|
|
eaf8b9cebc | ||
|
|
2ce9212457 | ||
|
|
4fa59ce258 | ||
|
|
320f79bb52 | ||
|
|
59ed89cba1 | ||
|
|
623b71715d | ||
|
|
a74943698f | ||
|
|
bfe8fd83ac | ||
|
|
0a01cac57b | ||
|
|
b7f248fa93 | ||
|
|
d814b6cdf0 | ||
|
|
1712bf60cb | ||
|
|
369a5a5233 | ||
|
|
9bae31d748 | ||
|
|
11453b327f | ||
|
|
7780c1c7b7 | ||
|
|
0f1c98d0d0 | ||
|
|
48fcbdc98c | ||
|
|
90d0196d47 | ||
|
|
484d2dc6cb | ||
|
|
30a8691c7f | ||
|
|
69cea6786d | ||
|
|
ccac5e73a3 | ||
|
|
3e79c8cf28 | ||
|
|
a63558a309 | ||
|
|
31dee1249d | ||
|
|
e5a293a6ab | ||
|
|
ffcb68b32f | ||
|
|
a8af90dfee | ||
|
|
6ee38cb291 | ||
|
|
3810df1b20 | ||
|
|
cc8e345a17 | ||
|
|
63653411b8 | ||
|
|
c4a6011621 | ||
|
|
1ee52c780f | ||
|
|
b402662c09 | ||
|
|
51756d45d9 | ||
|
|
a3bb7ad85a | ||
|
|
17058299c1 | ||
|
|
65de02d8d9 | ||
|
|
1f9e222d6e | ||
|
|
3d96be6c27 | ||
|
|
1bbe495306 | ||
|
|
a76f3ca1b3 | ||
|
|
7800b7db32 | ||
|
|
ea2a355bcc | ||
|
|
d585b75514 | ||
|
|
5caa32b1e0 | ||
|
|
11402d7819 | ||
|
|
a41f705dad | ||
|
|
69b808e62c | ||
|
|
1e1e629891 | ||
|
|
a7e96d055c | ||
|
|
5e2261f793 | ||
|
|
206be5c16f | ||
|
|
41c87efc5a | ||
|
|
171af35d85 | ||
|
|
99f47dc1ae | ||
|
|
cc7a516eba | ||
|
|
26436f116f | ||
|
|
9eab415906 | ||
|
|
14655e5633 | ||
|
|
51dc8d1de6 | ||
|
|
51568e6c56 | ||
|
|
d2743f4121 | ||
|
|
05c50e78bc | ||
|
|
9ac7b29e96 | ||
|
|
42106a72b3 | ||
|
|
2504046e26 | ||
|
|
a104f36561 | ||
|
|
b26afdcf2e | ||
|
|
bf707ae02d | ||
|
|
68648d2f6c | ||
|
|
371b8bf9cc | ||
|
|
3b89471b87 | ||
|
|
8a2117f9d4 | ||
|
|
132ddd3648 | ||
|
|
7aa26a950d | ||
|
|
b74be0b8f1 | ||
|
|
048aaee40d | ||
|
|
8caa559812 | ||
|
|
04475110ce | ||
|
|
02366eb27f | ||
|
|
6c3953e855 | ||
|
|
201286f59a | ||
|
|
b00bffd785 | ||
|
|
d477b55071 | ||
|
|
227b7ddba0 | ||
|
|
22fc67c8c3 | ||
|
|
d12f570178 | ||
|
|
70dc660f5a | ||
|
|
e4c379963f | ||
|
|
e44be63586 | ||
|
|
6d5436c885 | ||
|
|
d75abcf6a7 | ||
|
|
b3229785a0 | ||
|
|
bd8757bbb8 | ||
|
|
92bafa7c38 | ||
|
|
df756076e8 | ||
|
|
ffc9eecbd1 | ||
|
|
678efd1e8b | ||
|
|
cb9ac0d939 | ||
|
|
f513473362 | ||
|
|
9ab82621b9 | ||
|
|
59c2c7e343 | ||
|
|
8c3c0b2128 | ||
|
|
954581093d | ||
|
|
78afae4013 | ||
|
|
7811c58214 | ||
|
|
85a8f6b7cf | ||
|
|
f60e750848 | ||
|
|
a086f36433 | ||
|
|
4cb49c0b4a | ||
|
|
334587474f | ||
|
|
0d52737c49 | ||
|
|
d4dc080231 | ||
|
|
f8d35c3dcf | ||
|
|
c20b9fa5fa | ||
|
|
6a8fa727a9 | ||
|
|
7712a8bd10 | ||
|
|
4feb8fd1f1 | ||
|
|
994876911a | ||
|
|
d6398bd8fc | ||
|
|
43064b617a | ||
|
|
4fb9e75394 | ||
|
|
ee348f5585 | ||
|
|
eaca151a9f | ||
|
|
789783a370 | ||
|
|
9db1197c19 | ||
|
|
56a71c2cd8 | ||
|
|
e3ea22f479 | ||
|
|
5a017616f5 | ||
|
|
608c97603b | ||
|
|
a9721e7744 | ||
|
|
44e5af0434 | ||
|
|
dfbdc94e61 | ||
|
|
71d3427879 | ||
|
|
159fdf83ad | ||
|
|
d235ff1035 | ||
|
|
b2a359ca59 | ||
|
|
ee5be7f339 | ||
|
|
9b0e8b265d | ||
|
|
c0f243cee0 | ||
|
|
1bd2033a63 | ||
|
|
cae3748995 | ||
|
|
982f0d8f77 | ||
|
|
49e64b3d4c | ||
|
|
9e26b81adf | ||
|
|
94cc26aead | ||
|
|
d4b4007d96 | ||
|
|
2daf5c8fde | ||
|
|
7a5d4cedf6 | ||
|
|
2abc57f981 | ||
|
|
2d5894c5d6 | ||
|
|
0d43c0d326 | ||
|
|
1b46e159da | ||
|
|
a4d6fece41 | ||
|
|
86b24a4ccf | ||
|
|
8520cdd1bb | ||
|
|
0655617a9e | ||
|
|
4dbc8ab31e | ||
|
|
1a376a1a9b | ||
|
|
6e82964bf2 | ||
|
|
fdd5ffd45c | ||
|
|
ccb5c48c7d | ||
|
|
074d315c9f | ||
|
|
b8734fcc6c | ||
|
|
a8229f325d | ||
|
|
5768edb3a5 | ||
|
|
8bc80d2821 | ||
|
|
825f5ff88d | ||
|
|
5aee42d59d | ||
|
|
c8be764f35 | ||
|
|
4e2f7c95e3 | ||
|
|
56cd3a9949 | ||
|
|
12ce21cd08 | ||
|
|
ae5496f306 | ||
|
|
d23638c30d | ||
|
|
5724fad813 | ||
|
|
ffe54f591c | ||
|
|
a7e0f3df7b | ||
|
|
ab9cef689d | ||
|
|
18d68bbdf3 | ||
|
|
48436694eb | ||
|
|
16178b6f09 | ||
|
|
c2b6032b6f | ||
|
|
a44e5f9505 | ||
|
|
28ddb93747 | ||
|
|
2b0fa9bae6 | ||
|
|
16d54645bc | ||
|
|
dec7c45310 | ||
|
|
1a360d3ee7 | ||
|
|
584a37d2a2 | ||
|
|
cd023b621a | ||
|
|
7fdc2b5e66 | ||
|
|
1e7779a819 | ||
|
|
56478fcb8a | ||
|
|
e179d351d9 | ||
|
|
25554209ec | ||
|
|
d1a5921c27 | ||
|
|
ff14eb1d5a | ||
|
|
8fcd05b95f | ||
|
|
3a0882a1a9 | ||
|
|
5bd845d32b |
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -4,11 +4,12 @@ labels: ["triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Before opening a new issue, please ensure you are on the latest
|
||||
version (it might've already been fixed), and that you've searched
|
||||
for existing issues (please add you observations as a comment
|
||||
there instead of creating a duplicate).
|
||||
value: |
|
||||
Before opening a new bug report, please ensure
|
||||
1. you are on the latest version (it might've already been fixed),
|
||||
2. you've searched for existing issues (please add your observations as a comment there instead of creating a duplicate).
|
||||
|
||||
If you are self hosting, please create a community [Q&A](https://github.com/ente-io/ente/discussions/categories/q-a) instead.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
@@ -16,7 +17,8 @@ body:
|
||||
Please describe the bug. If possible, also include the steps to
|
||||
reproduce the behaviour, and the expected behaviour (sometimes
|
||||
bugs are just expectation mismatches, in which case this would be
|
||||
a good fit for Discussions).
|
||||
a good fit for [feature
|
||||
requests](https://github.com/ente-io/ente/discussions/categories/feature-requests)).
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.3"
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.github/workflows/mobile-lint.yml
vendored
@@ -9,7 +9,8 @@ on:
|
||||
- ".github/workflows/mobile-lint.yml"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.5"
|
||||
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
2
.github/workflows/mobile-release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
- "photos-v*"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.19.3"
|
||||
FLUTTER_VERSION: "3.19.4"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -12,9 +12,10 @@ There are many ways to contribute, and most of them don't require writing code.
|
||||
|
||||
## Spread the word
|
||||
|
||||
This is perhaps the most impactful contribution you can make. Spread the word.
|
||||
Online on your favorite social media channels. Offline to your friends and
|
||||
family who are looking for a privacy-friendly alternative to big tech.
|
||||
This is perhaps the most impactful contribution you can make. [Spread the
|
||||
word](https://help.ente.io/photos/features/referral-program/). Online on your
|
||||
favorite social media channels. Offline to your friends and family who are
|
||||
looking for a privacy-friendly alternative to big tech.
|
||||
|
||||
## Engage with the community
|
||||
|
||||
@@ -76,7 +77,10 @@ us](https://github.com/ente-io/ente/discussions). Discussing your idea with us
|
||||
first ensures that everyone is on the same page before you start working on your
|
||||
change.
|
||||
|
||||
## Star
|
||||
## Leave a review or star
|
||||
|
||||
If you haven't already done so, consider [starring this
|
||||
repository](https://github.com/ente-io/ente/stargazers).
|
||||
repository](https://github.com/ente-io/ente/stargazers) or leaving a review on
|
||||
[PlayStore](https://play.google.com/store/apps/details?id=io.ente.auth),
|
||||
[AppStore](https://apps.apple.com/us/app/ente-authenticator/id6444121398) or
|
||||
[AlternativeTo](https://alternativeto.net/software/ente-authenticator/).
|
||||
|
||||
@@ -60,8 +60,8 @@ Our labour of love. Two years ago, while building Ente Photos, we realized that
|
||||
there was no open source end-to-end encrypted authenticator app. We already had
|
||||
the building blocks, so we built one.
|
||||
|
||||
Ente Auth is currently free. If in the future we convert this to a paid service,
|
||||
existing users will be grandfathered in.
|
||||
Ente Auth is free, and will remain free forever. If you like the service and
|
||||
want to give back, please check out Ente Photos or spread the word.
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
@@ -95,13 +95,10 @@ more, see [docs/adding-icons](docs/adding-icons.md).
|
||||
|
||||
## 💚 Contribute
|
||||
|
||||
For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
The best way to support this project is by checking out [Ente
|
||||
Photos](../mobile/README.md) or spreading the word.
|
||||
|
||||
You can also support us by giving this project a ⭐ star on GitHub or by leaving
|
||||
a review on
|
||||
[PlayStore](https://play.google.com/store/apps/details?id=io.ente.auth),
|
||||
[AppStore](https://apps.apple.com/us/app/ente-authenticator/id6444121398) or
|
||||
[AlternativeTo](https://alternativeto.net/software/ente-authenticator/).
|
||||
For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
## ⭐️ About
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
},
|
||||
{
|
||||
"title": "Bloom Host",
|
||||
"slug": "bloom_host"
|
||||
"slug": "bloom_host",
|
||||
"altNames": [
|
||||
"Bloom Host Billing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "BorgBase",
|
||||
@@ -83,6 +86,9 @@
|
||||
{
|
||||
"title": "Discourse"
|
||||
},
|
||||
{
|
||||
"title": "Doppler"
|
||||
},
|
||||
{
|
||||
"title": "dus.net",
|
||||
"slug": "dusnet"
|
||||
@@ -190,6 +196,15 @@
|
||||
{
|
||||
"title": "Letterboxd"
|
||||
},
|
||||
{
|
||||
"title": "Local",
|
||||
"slug": "local_wp",
|
||||
"altNames": [
|
||||
"LocalWP",
|
||||
"Local WP",
|
||||
"Local Wordpress"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mastodon",
|
||||
"altNames": [
|
||||
@@ -203,7 +218,12 @@
|
||||
},
|
||||
{
|
||||
"title": "Mercado Livre",
|
||||
"slug": "mercado_livre"
|
||||
"slug": "mercado_livre",
|
||||
"altNames": [
|
||||
"Mercado Libre",
|
||||
"MercadoLibre",
|
||||
"MercadoLivre"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Murena",
|
||||
@@ -302,6 +322,10 @@
|
||||
"title": "Rockstar Games",
|
||||
"slug": "rockstar_games"
|
||||
},
|
||||
{
|
||||
"title": "RuneMate",
|
||||
"hex": "2ECC71"
|
||||
},
|
||||
{
|
||||
"title": "Rust Language Forum",
|
||||
"slug": "rust_language_forum",
|
||||
@@ -341,7 +365,10 @@
|
||||
"hex": "FFFFFF"
|
||||
},
|
||||
{
|
||||
"title": "Techlore"
|
||||
"title": "Techlore",
|
||||
"altNames": [
|
||||
"Techlore Courses"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Termius",
|
||||
@@ -412,6 +439,14 @@
|
||||
"Яндекс"
|
||||
],
|
||||
"slug": "Yandex"
|
||||
},
|
||||
{
|
||||
"title": "YNAB",
|
||||
"altNames": [
|
||||
"You Need A Budget"
|
||||
],
|
||||
"slug": "ynab",
|
||||
"hex": "3B5EDA"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
1
auth/assets/custom-icons/icons/doppler.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="800" viewBox="0 0 800 800" width="800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><radialGradient id="a" cx="0" cy="0" gradientTransform="matrix(-423.0004 -300.00003 172.7003 -243.50762 861 448)" gradientUnits="userSpaceOnUse" r="1"><stop offset="0" stop-color="#ea5926"/><stop offset="1" stop-color="#ea5a25" stop-opacity="0"/></radialGradient><radialGradient id="b" cx="0" cy="0" gradientTransform="matrix(-318.99928 -110.0022 110.0022 -318.99928 800 736)" gradientUnits="userSpaceOnUse" r="1"><stop offset="0" stop-color="#ea5a25"/><stop offset="1" stop-color="#ed5a21" stop-opacity="0"/></radialGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="505" x2="1.46826" y1="-305" y2="800.669"><stop offset="0" stop-color="#ff9efa"/><stop offset=".426562" stop-color="#ac50f7"/><stop offset=".646435" stop-color="#6b13f5"/></linearGradient><clipPath id="d"><path d="m0 0h500v500h-500z" transform="translate(150 150)"/></clipPath><rect fill="url(#c)" height="800" rx="400" width="800"/><rect fill="url(#a)" height="800" rx="400" width="800"/><rect fill="url(#b)" height="800" rx="400" width="800"/><g clip-path="url(#d)" fill="#fff"><path d="m467.396 151.3c-21.021-5.632-42.449 7.589-46.843 28.903l-19.94 96.716c-13.025 63.174-62.376 112.549-125.545 125.603l-94.873 19.607c-21.312 4.405-34.527 25.833-28.894 46.854 5.633 21.023 27.796 32.974 48.456 26.128l92.327-30.593c61.386-20.341 128.989-2.227 171.981 46.082l64.666 72.664c14.467 16.255 39.63 16.987 55.017 1.6 15.39-15.389 14.655-40.558-1.607-55.023l-73.135-65.056c-48.44-43.09-66.604-110.866-46.202-172.405l30.71-92.633c6.847-20.655-5.099-42.815-26.118-48.447z"/><path d="m216.103 272.283c-17.191-15.554-17.86-42.331-1.467-58.723 16.393-16.393 43.169-15.724 58.723 1.467l48.898 54.045c13.189 14.578 12.631 36.937-1.27 50.838s-36.261 14.46-50.839 1.271zm380.232 29.881c22.065-7.11 45.589 5.698 51.589 28.091s-7.967 45.248-30.632 50.122l-71.253 15.325c-19.22 4.133-38.305-7.53-43.393-26.52-5.088-18.989 5.608-38.632 24.32-44.662zm-217.826 315.811c-4.875 22.664-27.73 36.632-50.122 30.632-22.393-6-35.202-29.524-28.091-51.589l22.355-69.37c6.03-18.711 25.673-29.407 44.663-24.319 18.989 5.088 30.653 24.173 26.519 43.392z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
9
auth/assets/custom-icons/icons/local_wp.svg
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 117 KiB |
8
auth/assets/custom-icons/icons/runemate.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg" fill="#2ecc71">
|
||||
<symbol id="a" viewBox="0 0 1000 1000" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision">
|
||||
<path d="M456.557 954.142c-51.209-5.834-105.636-23.11-164.89-52.34-77.345-38.153-130.132-84.108-171.904-149.652-13.757-21.586-34.244-56.915-41.287-71.198-26.53-53.794-41.318-128.813-39.427-200 3.104-116.883 48.87-223.866 132.312-309.3C245.728 95.508 343.98 48.75 449.405 39.326c16.075-1.437 42.857-1.633 42.857-.314 0 .84-9.912 36.005-15.619 55.41-2.335 7.94-2.459 8.099-5.794 7.366-5.02-1.103-38.597 1.5-54.588 4.23-78.184 13.352-148.872 53.08-205.858 115.696-44.404 48.79-73.222 96.544-90.72 150.33-9.247 28.422-13.436 51.91-15.467 86.718-4.603 78.927 3.852 137.972 26.617 185.88 11.44 24.075 41.212 74.137 53.452 89.88 12.95 16.658 45.5 53.571 47.225 53.557 1.003-.008 6.645-1.359 12.538-3.002 33.243-9.268 71.782-16.19 110.119-19.778 23.046-2.158 81.589-1.805 105.952.637 80.53 8.074 152.793 28.04 218.763 60.442 14.585 7.164 22.502 10.436 24.057 9.942 4.456-1.414 34.073-21.892 46.466-32.127 36.789-30.385 55.405-51.859 83.62-96.456 12.304-19.447 33.186-60.968 40.115-79.762 11.156-30.26 17.905-59.183 20.418-87.5 1.58-17.803.844-74.678-1.255-97.024-5.875-62.539-19.14-96.584-65.864-169.047-18.12-28.102-30.924-44.798-47.526-61.974-33.977-35.15-76.434-63.926-121.175-82.126-7.53-3.063-13.868-5.732-14.086-5.932-.217-.2 2.595-13.79 6.25-30.202 3.655-16.412 6.646-30.065 6.646-30.34 0-.699 20.397 5.54 29.894 9.143 48.79 18.51 94.215 50.387 138.598 97.264 28.447 30.046 56.905 68.452 77.93 105.172 23.918 41.775 43.482 101.062 51.717 156.733 12.077 81.642 3.119 156.375-27.337 228.05-26.303 61.906-59.852 113.54-102.193 157.284-24.676 25.493-52.838 48.34-76.347 61.935-5.239 3.03-14.647 8.58-20.908 12.333-39.579 23.728-89.618 43.541-136.83 54.177-30.244 6.813-50.423 9.032-86.31 9.49-23.602.3-37.675-.07-48.205-1.27z"/>
|
||||
<path d="M500.595 722.575c-49.27-3.036-105.94-12.2-150.595-24.354-49.438-13.455-103.365-35.123-141.369-56.803-12.536-7.15-12.797-7.387-12.786-11.562.006-2.343 1.876-23.814 4.155-47.713 2.28-23.899 4.15-44.297 4.155-45.328.01-1.554-3.775-2.292-22.044-4.298-12.131-1.332-22.383-2.75-22.782-3.148-.891-.891 9.648-113.624 10.723-114.698.423-.423 11.314.114 24.203 1.193 12.89 1.08 23.606 1.79 23.816 1.581.27-.27 14.667-126.689 14.667-128.792 0-.604 20.651-7.884 29.762-10.492 34.467-9.866 97.903-19.388 152.496-22.89l12.614-.81 1.922-15.477c1.056-8.512 2.076-15.622 2.266-15.799.19-.177 12.08.182 26.425.797 14.344.616 26.479.695 26.966.176.487-.52 15.317-42.194 32.956-92.61 31.657-90.487 32.112-91.672 35.394-92.059 4.302-.507 31.608 2.981 39.318 5.023 3.274.867 5.892 2.055 5.818 2.64-.073.584-8.608 42.17-18.966 92.413-10.358 50.243-18.592 91.591-18.298 91.886.294.294 13.353 2.995 29.019 6.003 15.666 3.007 28.707 5.69 28.979 5.963.272.272-.376 8.106-1.44 17.409-1.065 9.303-1.681 17.168-1.37 17.479.31.31 6.685 1.808 14.166 3.328 27.185 5.523 59.165 15.788 92.694 29.752 18.402 7.665 72.77 34.438 74.732 36.802.998 1.202-.2 17.922-4.742 66.131-3.342 35.487-5.827 64.772-5.52 65.079.306.306 10.781 1.422 23.278 2.48 19.132 1.62 22.722 2.221 22.722 3.81 0 3.471-10.586 114.581-10.95 114.934-.198.192-11.208-.558-24.467-1.666-16.147-1.35-24.13-1.608-24.176-.783-.038.678-2.583 22.36-5.656 48.182l-5.587 46.95-15.475 3.924c-48.273 12.242-94.678 20.005-144.642 24.197-20.767 1.742-81.67 2.427-102.38 1.15zm83.334-73.78c32.215-2.204 65.058-6.745 96.16-13.295 10.325-2.175 12.312-2.962 12.758-5.05.29-1.36 5.928-54.426 12.529-117.924 11.371-109.385 11.891-115.53 9.9-116.986-4.135-3.024-25.054-13.273-39.086-19.15-47.244-19.786-97.576-32.17-158.928-39.105-26.75-3.023-100.774-3.013-129.096.019-24.927 2.667-45.3 5.631-67.127 9.764l-16.763 3.175-.677 4.938c-2.761 20.122-25.65 226.992-25.376 229.34.296 2.531 2.763 4.19 16.42 11.041C351.133 623.9 427.686 643.701 500 648.678c20.699 1.425 63.937 1.485 83.929.117z"/>
|
||||
<path d="M441.221 506.496c-2.209-.959-9.779-7.098-16.821-13.644-7.043-6.545-13.208-11.9-13.7-11.9-.494 0-7.17 4.746-14.838 10.547-9.216 6.972-15.442 10.829-18.369 11.378-12.159 2.28-25.707-8.772-25.707-20.972 0-10.383 3.31-14.929 22.964-31.532 20.158-17.029 29.05-20.901 41.876-18.239 9.676 2.01 17.414 8.276 33.054 26.77 15.838 18.73 19.347 24.737 19.36 33.152.007 5.098-.483 6.196-4.344 9.734-6.791 6.223-15.813 8.032-23.475 4.706zm171.214 16.261c-2.91-1.89-9.834-8.05-15.385-13.69-5.55-5.642-10.54-10.257-11.088-10.257-.547 0-6.88 4.576-14.073 10.17-14.107 10.97-16.86 12.449-23.195 12.449-6.057 0-11.222-2.58-16.19-8.084-7.604-8.428-8.187-17.083-1.791-26.58 4.035-5.992 31.416-29.148 38.829-32.838 12.562-6.252 26.061-4.757 37.253 4.125 3.083 2.446 11.776 11.884 19.318 20.973 14.946 18.01 19.353 26.332 17.938 33.874-2.207 11.769-20.08 17.341-31.616 9.858z"/>
|
||||
</symbol>
|
||||
<use xlink:href="#a" xmlns:xlink="http://www.w3.org/1999/xlink"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
20
auth/assets/custom-icons/icons/ynab.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="576" height="569" viewBox="0 0 576 569" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M464.493 260.334H359.255C357.022 260.334 354.236 261.878 353.078 263.765L290.011 366.435C288.854 368.323 286.969 368.313 285.812 366.435L223.411 263.786C222.264 261.898 219.488 260.344 217.255 260.344H111.526C109.293 260.344 108.473 261.838 109.723 263.675L234.392 446.992C235.641 448.819 236.655 452.129 236.655 454.33V564.377C236.655 566.577 238.489 568.383 240.722 568.383H335.255C337.488 568.383 339.322 566.577 339.322 564.377V454.33C339.322 452.129 340.346 448.819 341.586 446.992L466.285 263.675C467.535 261.848 466.715 260.344 464.482 260.344L464.493 260.334Z" fill="#3B5EDA"/>
|
||||
<path d="M280.775 101.732C283.521 103.236 286.184 104.457 287.587 104.457C288.99 104.457 291.347 103.62 294.379 101.732C353.983 64.118 304.386 17.9556 287.587 0.566406C270.778 17.9556 221.181 64.118 280.775 101.732Z" fill="#3B5EDA"/>
|
||||
<path d="M218.089 238.947C221.48 238.675 224.624 238.201 225.884 237.363C227.154 236.525 228.763 234.386 230.34 230.884C261.09 161.69 188.241 149.479 162.49 143.756C157.952 169.381 141.44 240.35 218.089 238.937V238.947Z" fill="#3B5EDA"/>
|
||||
<path d="M120.44 223.836C123.308 222.029 125.858 220.172 126.585 218.85C127.313 217.528 127.722 214.884 127.456 211.069C121.812 135.739 51.6162 158.488 26.1416 165.3C34.2951 190.037 53.3678 260.39 120.44 223.836V223.836Z" fill="#3B5EDA"/>
|
||||
<path d="M132.607 350.144C133.826 347.016 134.747 344.018 134.532 342.535C134.317 341.041 133.047 338.69 130.538 335.773C80.5514 278.509 37.8274 337.912 21.4487 358.309C42.9285 373.428 100.71 418.823 132.596 350.144H132.607Z" fill="#3B5EDA"/>
|
||||
<path d="M287.578 120.822C274.426 135.446 235.615 174.241 282.242 205.86C284.393 207.122 286.472 208.151 287.578 208.151C288.684 208.151 290.518 207.445 292.895 205.86C339.532 174.241 300.73 135.436 287.578 120.822Z" fill="#3B5EDA"/>
|
||||
<path d="M159.196 382.333C158.448 381.818 156.861 381.445 154.495 381.425C107.786 381.233 115.561 425.518 117.487 441.525C133.384 437.71 178.218 429.404 161.706 386.36C160.846 384.513 159.934 382.848 159.186 382.333H159.196Z" fill="#3B5EDA"/>
|
||||
<path d="M282.242 306.502C284.393 307.763 286.472 308.793 287.578 308.793C288.684 308.793 290.518 308.086 292.894 306.502C339.531 274.882 300.73 236.077 287.578 221.463C274.426 236.087 235.615 274.882 282.242 306.502Z" fill="#3B5EDA"/>
|
||||
<path d="M85.4788 282.937C86.9436 280.939 88.1727 278.991 88.2751 277.911C88.3776 276.831 87.8244 274.964 86.4416 272.492C58.6623 223.887 15.9587 258.494 0 270.09C13.6028 284.31 49.3517 325.87 85.4893 282.937H85.4788Z" fill="#3B5EDA"/>
|
||||
<path d="M133.537 149.101C136.036 148.93 138.341 148.607 139.242 147.981C140.143 147.355 141.25 145.751 142.284 143.116C162.279 90.9288 107.939 80.8768 88.6822 76.2646C86.3468 95.6621 76.9434 149.353 133.547 149.101H133.537Z" fill="#3B5EDA"/>
|
||||
<path d="M210.076 132.318C212.831 132.792 215.433 133.054 216.591 132.621C217.748 132.187 219.387 130.733 221.23 128.14C257.286 76.719 200.949 51.3166 181.262 41.1737C173.395 61.6511 148.453 117.522 210.076 132.318V132.318Z" fill="#3B5EDA"/>
|
||||
<path d="M349.542 237.37C350.812 238.207 353.946 238.672 357.337 238.954C433.976 240.367 417.464 169.398 412.937 143.773C387.185 149.486 314.336 161.707 345.086 230.901C346.664 234.393 348.272 236.542 349.542 237.38V237.37Z" fill="#3B5EDA"/>
|
||||
<path d="M448 211.065C447.734 214.879 448.143 217.524 448.871 218.846C449.598 220.168 452.148 222.035 455.016 223.832C522.098 260.376 541.171 190.032 549.314 165.296C523.839 158.494 453.644 135.735 448 211.065V211.065Z" fill="#3B5EDA"/>
|
||||
<path d="M444.868 335.773C442.359 338.69 441.089 341.051 440.874 342.535C440.659 344.018 441.59 347.016 442.809 350.144C474.696 418.823 532.478 373.428 553.957 358.309C537.579 337.912 494.865 278.509 444.868 335.773V335.773Z" fill="#3B5EDA"/>
|
||||
<path d="M420.923 381.408C418.557 381.428 416.969 381.811 416.222 382.316C415.474 382.821 414.562 384.486 413.702 386.343C397.2 429.387 442.024 437.693 457.921 441.508C459.847 425.511 467.621 381.216 420.913 381.408H420.923Z" fill="#3B5EDA"/>
|
||||
<path d="M488.991 272.502C487.598 274.974 487.055 276.841 487.158 277.921C487.26 279.001 488.479 280.949 489.954 282.947C526.092 325.87 561.84 284.32 575.443 270.099C559.484 258.513 516.781 223.907 489.002 272.502H488.991Z" fill="#3B5EDA"/>
|
||||
<path d="M436.185 147.981C437.086 148.596 439.391 148.919 441.89 149.101C498.493 149.353 489.091 95.6621 486.755 76.2646C467.498 80.8768 413.148 90.9389 433.153 143.116C434.177 145.751 435.283 147.365 436.195 147.981H436.185Z" fill="#3B5EDA"/>
|
||||
<path d="M358.842 132.621C359.999 133.054 362.591 132.792 365.357 132.318C426.979 117.522 402.037 61.6411 394.171 41.1737C374.494 51.3166 318.156 76.719 354.202 128.14C356.046 130.733 357.684 132.187 358.842 132.621V132.621Z" fill="#3B5EDA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -189,7 +189,7 @@ class _AppState extends State<App> with WindowListener, TrayListener {
|
||||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
windowManager.close();
|
||||
windowManager.destroy();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "Émetteur",
|
||||
"codeSecretKeyHint": "Clé secrète",
|
||||
"codeAccountHint": "Compte (vous@exemple.com)",
|
||||
"codeTagHint": "Tag",
|
||||
"accountKeyType": "Type de clé",
|
||||
"sessionExpired": "Session expirée",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -77,12 +79,14 @@
|
||||
"data": "Données",
|
||||
"importCodes": "Importer les codes",
|
||||
"importTypePlainText": "Texte brut",
|
||||
"importTypeEnteEncrypted": "Export chiffré Ente",
|
||||
"passwordForDecryptingExport": "Mot de passe pour déchiffrer l'exportation",
|
||||
"passwordEmptyError": "Le mot de passe ne peut pas être vide",
|
||||
"importFromApp": "Importer des codes depuis {appName}",
|
||||
"importGoogleAuthGuide": "Exportez vos comptes depuis Google Authenticator vers un code QR en utilisant l'option \"Transférer des comptes\". Ensuite, en utilisant un autre appareil, scannez le code QR.\n\nAstuce : Vous pouvez utiliser la webcam de votre ordinateur portable pour prendre une photo du code QR.",
|
||||
"importSelectJsonFile": "Sélectionnez un fichier JSON",
|
||||
"importSelectAppExport": "Sélectionnez le fichier d'exportation {appName}",
|
||||
"importEnteEncGuide": "Sélectionnez le fichier chiffré JSON exporté depuis Ente",
|
||||
"importRaivoGuide": "Utilisez l'option \"Exporter les OTPs vers l'archive Zip\" dans les paramètres de Raivo.\n\nExtrayez le fichier zip et importez le fichier JSON.",
|
||||
"importBitwardenGuide": "Utilisez l'option « Exporter le coffre » dans les outils Bitwarden et importez le fichier JSON non chiffré.",
|
||||
"importAegisGuide": "Utilisez l'option \"Exporter le coffre-fort\" dans les paramètres d'Aegis.\n\nSi votre coffre-fort est crypté, vous devrez saisir le mot de passe du coffre-fort pour déchiffrer le coffre-fort.",
|
||||
@@ -112,18 +116,22 @@
|
||||
"copied": "Copié",
|
||||
"pleaseTryAgain": "Veuillez réessayer",
|
||||
"existingUser": "Utilisateur existant",
|
||||
"newUser": "Nouveau dans Ente",
|
||||
"delete": "Supprimer",
|
||||
"enterYourPasswordHint": "Saisir votre mot de passe",
|
||||
"forgotPassword": "Mot de passe oublié",
|
||||
"oops": "Oups",
|
||||
"suggestFeatures": "Suggérer des fonctionnalités",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "Quelle est la sécurité de Auth?",
|
||||
"faq_a_1": "Tous les codes que vous sauvegardez via ente sont chiffrés de bout en bout. Cela signifie que vous seul pouvez accéder à vos codes. Nos applications sont open source et notre cryptographie ont fait l'objet d'un audit externe.",
|
||||
"faq_q_2": "Puis-je accéder à mes codes sur mon ordinateur ?",
|
||||
"faq_a_2": "Vous pouvez accéder à vos codes sur le web via auth.ente.io.",
|
||||
"faq_q_3": "Comment puis-je supprimer des codes ?",
|
||||
"faq_a_3": "Vous pouvez supprimer un code en glissant vers la gauche.",
|
||||
"faq_q_4": "Comment puis-je soutenir le projet ?",
|
||||
"faq_a_4": "Vous pouvez soutenir le développement de ce projet en vous abonnant à notre application Photos, ente.io.",
|
||||
"faq_q_5": "Comment puis-je activer le verrouillage FaceID dans Auth",
|
||||
"faq_a_5": "Vous pouvez activer le verrouillage FaceID dans Paramètres → Sécurité → Écran de verrouillage.",
|
||||
"somethingWentWrongMessage": "Quelque chose s'est mal passé, veuillez recommencer",
|
||||
"leaveFamily": "Quitter le plan familial",
|
||||
@@ -150,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "QR code non valide",
|
||||
"noRecoveryKeyTitle": "Pas de clé de récupération ?",
|
||||
"enterEmailHint": "Entrez votre adresse e-mail",
|
||||
"invalidEmailTitle": "Adresse e-mail invalide",
|
||||
@@ -343,6 +352,7 @@
|
||||
"deleteCodeAuthMessage": "Authentification requise pour supprimer le code",
|
||||
"showQRAuthMessage": "Authentification requise pour afficher le code QR",
|
||||
"confirmAccountDeleteTitle": "Confirmer la suppression du compte",
|
||||
"confirmAccountDeleteMessage": "Ce compte est lié à d'autres applications ente, si vous en utilisez une.\n\nVos données téléchargées, dans toutes les applications ente, seront planifiées pour suppression, et votre compte sera définitivement supprimé.",
|
||||
"androidBiometricHint": "Vérifier l’identité",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -413,5 +423,18 @@
|
||||
"invalidEndpoint": "Point de terminaison non valide",
|
||||
"invalidEndpointMessage": "Désolé, le point de terminaison que vous avez entré n'est pas valide. Veuillez en entrer un valide puis réessayez.",
|
||||
"endpointUpdatedMessage": "Point de terminaison mis à jour avec succès",
|
||||
"customEndpoint": "Connecté à {endpoint}"
|
||||
"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",
|
||||
"create": "Créer",
|
||||
"editTag": "Modifier le tag",
|
||||
"deleteTagTitle": "Supprimer le tag ?",
|
||||
"deleteTagMessage": "Êtes-vous sûr de vouloir supprimer ce tag ? Cette action est irréversible.",
|
||||
"somethingWentWrongParsingCode": "Impossible d'analyser les codes {x}.",
|
||||
"updateNotAvailable": "Mise à jour non disponible"
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "発行者",
|
||||
"codeSecretKeyHint": "秘密鍵",
|
||||
"codeAccountHint": "アカウント (you@domain.com)",
|
||||
"codeTagHint": "タグ",
|
||||
"accountKeyType": "鍵の種類",
|
||||
"sessionExpired": "セッションが失効しました",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -77,6 +79,7 @@
|
||||
"data": "データ",
|
||||
"importCodes": "コードをインポート",
|
||||
"importTypePlainText": "プレーンテキスト",
|
||||
"importTypeEnteEncrypted": "Ente 暗号化されたエクスポート",
|
||||
"passwordForDecryptingExport": "復号化用パスワード",
|
||||
"passwordEmptyError": "パスワードは空欄にできません",
|
||||
"importFromApp": "{appName} からコードをインポート",
|
||||
@@ -121,6 +124,7 @@
|
||||
"suggestFeatures": "機能を提案",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "Authはどのくらい安全ですか?",
|
||||
"faq_a_1": "Ente Authでバックアップされたコードはすべてエンドツーエンドで暗号化されて保存されます。つまり、コードにアクセスできるのはあなただけです。当社のアプリはオープンソースであり、暗号化技術は外部監査を受けています。",
|
||||
"faq_q_2": "パソコンから私のコードにアクセスできますか?",
|
||||
"faq_a_2": "auth.ente.io で Web からコードにアクセス可能です。",
|
||||
"faq_q_3": "コードを削除するにはどうすればいいですか?",
|
||||
@@ -154,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "QRコードが無効です",
|
||||
"noRecoveryKeyTitle": "回復キーがありませんか?",
|
||||
"enterEmailHint": "メールアドレスを入力してください",
|
||||
"invalidEmailTitle": "メールアドレスが無効です",
|
||||
@@ -347,6 +352,7 @@
|
||||
"deleteCodeAuthMessage": "コードを削除するためには認証が必要です",
|
||||
"showQRAuthMessage": "QR コードを表示するためには認証が必要です",
|
||||
"confirmAccountDeleteTitle": "アカウントの削除に同意",
|
||||
"confirmAccountDeleteMessage": "このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。\nすべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。",
|
||||
"androidBiometricHint": "本人を確認する",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -417,5 +423,18 @@
|
||||
"invalidEndpoint": "無効なエンドポイントです",
|
||||
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
|
||||
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
|
||||
"customEndpoint": "{endpoint} に接続しました"
|
||||
"customEndpoint": "{endpoint} に接続しました",
|
||||
"pinText": "固定",
|
||||
"unpinText": "固定を解除",
|
||||
"pinnedCodeMessage": "{code} を固定しました",
|
||||
"unpinnedCodeMessage": "{code} の固定が解除されました",
|
||||
"tags": "タグ",
|
||||
"createNewTag": "新しいタグの作成",
|
||||
"tag": "タグ",
|
||||
"create": "作成",
|
||||
"editTag": "タグの編集",
|
||||
"deleteTagTitle": "タグを削除しますか?",
|
||||
"deleteTagMessage": "このタグを削除してもよろしいですか?この操作は取り消しできません。",
|
||||
"somethingWentWrongParsingCode": "{x} のコードを解析できませんでした。",
|
||||
"updateNotAvailable": "アップデートは利用できません"
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "Proteja seus códigos 2FA",
|
||||
"onBoardingGetStarted": "Vamos Começar",
|
||||
"onBoardingGetStarted": "Introdução",
|
||||
"setupFirstAccount": "Configure sua primeira conta",
|
||||
"importScanQrCode": "Escanear QR code",
|
||||
"qrCode": "QR Code",
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "Emissor",
|
||||
"codeSecretKeyHint": "Chave secreta",
|
||||
"codeAccountHint": "Conta (voce@dominio.com)",
|
||||
"codeTagHint": "Etiqueta",
|
||||
"accountKeyType": "Tipo de chave",
|
||||
"sessionExpired": "Sessão expirada",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -29,13 +31,13 @@
|
||||
"timeBasedKeyType": "Baseado no horário (TOTP)",
|
||||
"counterBasedKeyType": "Baseado em um contador (HOTP)",
|
||||
"saveAction": "Salvar",
|
||||
"nextTotpTitle": "próximo",
|
||||
"deleteCodeTitle": "Excluir código?",
|
||||
"nextTotpTitle": "avançar",
|
||||
"deleteCodeTitle": "Apagar código?",
|
||||
"deleteCodeMessage": "Tem certeza de que deseja excluir este código? Esta ação é irreversível.",
|
||||
"viewLogsAction": "Ver logs",
|
||||
"sendLogsDescription": "Isto irá compartilhar seus logs para nos ajudar a depurar seu problema. Embora tomemos precauções para garantir que informações sensíveis não sejam enviadas, encorajamos você a ver esses logs antes de compartilhá-los.",
|
||||
"preparingLogsTitle": "Preparando logs...",
|
||||
"emailLogsTitle": "Logs por e-mail",
|
||||
"emailLogsTitle": "Logs (e-mail)",
|
||||
"emailLogsMessage": "Por favor, envie os logs para {email}",
|
||||
"@emailLogsMessage": {
|
||||
"placeholders": {
|
||||
@@ -46,9 +48,9 @@
|
||||
},
|
||||
"copyEmailAction": "Copiar e-mail",
|
||||
"exportLogsAction": "Exportar logs",
|
||||
"reportABug": "Reportar um problema",
|
||||
"reportABug": "Informar um problema",
|
||||
"crashAndErrorReporting": "Reporte de erros e falhas",
|
||||
"reportBug": "Reportar problema",
|
||||
"reportBug": "Informar problema",
|
||||
"emailUsMessage": "Por favor, envie um e-mail para {email}",
|
||||
"@emailUsMessage": {
|
||||
"placeholders": {
|
||||
@@ -103,14 +105,14 @@
|
||||
"authToChangeYourPassword": "Por favor, autentique-se para alterar sua senha",
|
||||
"authToViewSecrets": "Por favor, autentique-se para ver as suas chaves secretas",
|
||||
"authToInitiateSignIn": "Por favor, autentique-se para iniciar o login para um backup.",
|
||||
"ok": "Ok",
|
||||
"ok": "OK",
|
||||
"cancel": "Cancelar",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"email": "E-mail",
|
||||
"support": "Suporte",
|
||||
"general": "Geral",
|
||||
"settings": "Configurações",
|
||||
"settings": "Ajustes",
|
||||
"copied": "Copiado",
|
||||
"pleaseTryAgain": "Por favor, tente novamente",
|
||||
"existingUser": "Usuário Existente",
|
||||
@@ -118,7 +120,7 @@
|
||||
"delete": "Excluir",
|
||||
"enterYourPasswordHint": "Insira sua senha",
|
||||
"forgotPassword": "Esqueci a senha",
|
||||
"oops": "Oops",
|
||||
"oops": "Opa",
|
||||
"suggestFeatures": "Sugerir funcionalidades",
|
||||
"faq": "Perguntas frequentes",
|
||||
"faq_q_1": "Quão seguro é o Auth?",
|
||||
@@ -137,7 +139,7 @@
|
||||
"inFamilyPlanMessage": "Você está em um plano familiar!",
|
||||
"swipeHint": "Deslize para a esquerda para editar ou remover os códigos",
|
||||
"scan": "Escanear",
|
||||
"scanACode": "Escanear um código",
|
||||
"scanACode": "Escanear código",
|
||||
"verify": "Verificar",
|
||||
"verifyEmail": "Verificar e-mail",
|
||||
"enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
|
||||
@@ -156,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "QR Code inválido",
|
||||
"noRecoveryKeyTitle": "Sem chave de recuperação?",
|
||||
"enterEmailHint": "Insira o seu endereço de e-mail",
|
||||
"invalidEmailTitle": "Endereço de e-mail inválido",
|
||||
@@ -182,7 +185,7 @@
|
||||
"lockScreenEnablePreSteps": "Para ativar o bloqueio de tela, por favor ative um método de autenticação nas configurações do sistema do seu dispositivo.",
|
||||
"viewActiveSessions": "Ver sessões ativas",
|
||||
"authToViewYourActiveSessions": "Por favor, autentique-se para ver as sessões ativas",
|
||||
"searchHint": "Pesquisar...",
|
||||
"searchHint": "Buscar...",
|
||||
"search": "Pesquisar",
|
||||
"sorryUnableToGenCode": "Desculpe, não foi possível gerar um código para {issuerName}",
|
||||
"noResult": "Nenhum resultado",
|
||||
@@ -236,10 +239,10 @@
|
||||
"howItWorks": "Como funciona",
|
||||
"ackPasswordLostWarning": "Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>.",
|
||||
"loginTerms": "Ao clicar em login, eu concordo com os <u-terms>termos de serviço</u-terms> e a <u-policy>política de privacidade</u-policy>",
|
||||
"logInLabel": "Login",
|
||||
"logout": "Encerrar sessão",
|
||||
"logInLabel": "Entrar",
|
||||
"logout": "Sair",
|
||||
"areYouSureYouWantToLogout": "Você tem certeza que deseja encerrar a sessão?",
|
||||
"yesLogout": "Sim, encerrar sessão",
|
||||
"yesLogout": "Sim, sair",
|
||||
"exit": "Sair",
|
||||
"verifyingRecoveryKey": "Verificando chave de recuperação...",
|
||||
"recoveryKeyVerified": "Chave de recuperação verificada",
|
||||
@@ -279,7 +282,7 @@
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
},
|
||||
"authToExportCodes": "Por favor, autentique-se para exportar seus códigos",
|
||||
"importSuccessTitle": "Yay!",
|
||||
"importSuccessTitle": "Oba!",
|
||||
"importSuccessDesc": "Você importou {count} códigos!",
|
||||
"@importSuccessDesc": {
|
||||
"placeholders": {
|
||||
@@ -314,7 +317,7 @@
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Isso fará com que você saia do seguinte dispositivo:",
|
||||
"terminateSession": "Encerrar sessão?",
|
||||
"terminate": "Encerrar",
|
||||
"thisDevice": "Este dispositivo",
|
||||
"thisDevice": "Esse dispositivo",
|
||||
"toResetVerifyEmail": "Para redefinir a sua senha, por favor verifique o seu email primeiro.",
|
||||
"thisEmailIsAlreadyInUse": "Este e-mail já está em uso",
|
||||
"verificationFailedPleaseTryAgain": "Falha na verificação. Por favor, tente novamente",
|
||||
@@ -336,7 +339,7 @@
|
||||
"export": "Exportar",
|
||||
"useOffline": "Usar sem backups",
|
||||
"signInToBackup": "Entre para fazer backup de seus códigos",
|
||||
"singIn": "Iniciar sessão",
|
||||
"singIn": "Entrar",
|
||||
"sigInBackupReminder": "Por favor, exporte seus códigos para garantir que você tenha um backup do qual você possa restaurar.",
|
||||
"offlineModeWarning": "Você escolheu prosseguir sem backups. Por favor, faça backups manuais para ter certeza de que seus códigos estão seguros.",
|
||||
"showLargeIcons": "Mostrar ícones grandes",
|
||||
@@ -358,7 +361,7 @@
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Bem-sucedido",
|
||||
"androidBiometricSuccess": "Êxito",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
@@ -398,7 +401,7 @@
|
||||
"@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."
|
||||
},
|
||||
"iOSOkButton": "Ok",
|
||||
"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."
|
||||
},
|
||||
@@ -420,5 +423,18 @@
|
||||
"invalidEndpoint": "Endpoint inválido",
|
||||
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
|
||||
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
|
||||
"customEndpoint": "Conectado a {endpoint}"
|
||||
"customEndpoint": "Conectado a {endpoint}",
|
||||
"pinText": "Fixar",
|
||||
"unpinText": "Desafixar",
|
||||
"pinnedCodeMessage": "{code} foi fixado",
|
||||
"unpinnedCodeMessage": "{code} foi desafixado",
|
||||
"tags": "Etiquetas",
|
||||
"createNewTag": "Criar etiqueta",
|
||||
"tag": "Etiqueta",
|
||||
"create": "Criar",
|
||||
"editTag": "Editar etiqueta",
|
||||
"deleteTagTitle": "Apagar etiqueta?",
|
||||
"deleteTagMessage": "Tem certeza de que deseja excluir esta etiqueta? Essa ação é irreversível.",
|
||||
"somethingWentWrongParsingCode": "Não foi possível analisar os códigos {x}.",
|
||||
"updateNotAvailable": "Atualização não está disponível"
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "Эмитент",
|
||||
"codeSecretKeyHint": "Секретный ключ",
|
||||
"codeAccountHint": "Аккаунт (you@domain.com)",
|
||||
"codeTagHint": "Метка",
|
||||
"accountKeyType": "Тип ключа",
|
||||
"sessionExpired": "Сеанс истек",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -77,16 +79,19 @@
|
||||
"data": "Данные",
|
||||
"importCodes": "Импортировать коды",
|
||||
"importTypePlainText": "Обычный текст",
|
||||
"importTypeEnteEncrypted": "Ente Зашифрованный экспорт",
|
||||
"passwordForDecryptingExport": "Пароль для расшифровки экспорта",
|
||||
"passwordEmptyError": "Пароль не может быть пустым",
|
||||
"importFromApp": "Импорт кодов из {appName}",
|
||||
"importGoogleAuthGuide": "Экспортируйте учетные записи из Google Authenticator в QR-код, используя опцию «Перенести учетные записи». Затем с помощью другого устройства отсканируйте QR-код.\n\nСовет: Чтобы сфотографировать QR-код, можно воспользоваться веб-камерой ноутбука.",
|
||||
"importSelectJsonFile": "Выбрать JSON-файл",
|
||||
"importSelectAppExport": "Выбрать файл экспорта {appName}",
|
||||
"importEnteEncGuide": "Выберите зашифрованный JSON файл, экспортированный из Ente",
|
||||
"importRaivoGuide": "Используйте опцию «Export OTPs to Zip archive» в настройках Raivo.\n\nРаспакуйте zip-архив и импортируйте JSON-файл.",
|
||||
"importBitwardenGuide": "Используйте опцию \"Экспортировать хранилище\" в Bitwarden Tools и импортируйте незашифрованный JSON файл.",
|
||||
"importAegisGuide": "Используйте опцию «Экспортировать хранилище» в настройках Aegis.\n\nЕсли ваше хранилище зашифровано, то для его расшифровки потребуется ввести пароль хранилища.",
|
||||
"import2FasGuide": "Используйте опцию \"Settings->Backup -Export\" в 2FAS.\n\nЕсли ваша резервная копия зашифрована, то для расшифровки резервной копии необходимо ввести пароль",
|
||||
"importLastpassGuide": "Используйте опцию \"Перенести аккаунты\" в настройках Lastpass Authenticator и нажмите на \"Экспортировать учетные записи в файл\". Импортируйте загружённый JSON файл.",
|
||||
"exportCodes": "Экспортировать коды",
|
||||
"importLabel": "Импорт",
|
||||
"importInstruction": "Пожалуйста, выберите файл, содержащий список ваших кодов в следующем формате",
|
||||
@@ -99,6 +104,7 @@
|
||||
"authToChangeYourEmail": "Пожалуйста, авторизуйтесь, чтобы изменить адрес электронной почты",
|
||||
"authToChangeYourPassword": "Пожалуйста, авторизуйтесь, чтобы изменить пароль",
|
||||
"authToViewSecrets": "Пожалуйста, авторизуйтесь для просмотра ваших секретов",
|
||||
"authToInitiateSignIn": "Пожалуйста, авторизуйтесь, чтобы начать вход для резервного копирования.",
|
||||
"ok": "Ок",
|
||||
"cancel": "Отменить",
|
||||
"yes": "Да",
|
||||
@@ -110,18 +116,22 @@
|
||||
"copied": "Скопировано",
|
||||
"pleaseTryAgain": "Пожалуйста, попробуйте ещё раз",
|
||||
"existingUser": "Существующий пользователь",
|
||||
"newUser": "Впервые здесь, в Ente",
|
||||
"delete": "Удалить",
|
||||
"enterYourPasswordHint": "Введите пароль",
|
||||
"forgotPassword": "Забыл пароль",
|
||||
"oops": "Ой",
|
||||
"suggestFeatures": "Предложить идеи",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "Насколько безопасен Auth?",
|
||||
"faq_a_1": "Все коды, которые вы резервируете с помощью Auth, хранятся в зашифрованном виде. Это означает, что только вы можете получить доступ к своим кодам. Наши приложения имеют открытый исходный код, а наша криптография прошла внешний аудит.",
|
||||
"faq_q_2": "Могу ли я получить доступ к моим кодам на компьютере?",
|
||||
"faq_a_2": "Вы можете получить доступ к своим кодам на сайте @ auth.ente.io.",
|
||||
"faq_q_3": "Как я могу удалить коды?",
|
||||
"faq_a_3": "Вы можете удалить код, проведя пальцем влево по этому элементу.",
|
||||
"faq_q_4": "Как я могу поддержать этот проект?",
|
||||
"faq_a_4": "Вы можете поддержать развитие этого проекта, подписавшись на наше приложение Photos @ ente.io.",
|
||||
"faq_q_5": "Как мне включить FaceID в Auth",
|
||||
"faq_a_5": "Вы можете включить блокировку FaceID в Настройки → Безопасность → Экран блокировки.",
|
||||
"somethingWentWrongMessage": "Что-то пошло не так. Попробуйте еще раз",
|
||||
"leaveFamily": "Покинуть семью",
|
||||
@@ -135,6 +145,8 @@
|
||||
"enterCodeHint": "Введите 6-значный код из\nвашего приложения-аутентификатора",
|
||||
"lostDeviceTitle": "Потеряно устройство?",
|
||||
"twoFactorAuthTitle": "Двухфакторная аутентификация",
|
||||
"passkeyAuthTitle": "Проверка с помощью пароля",
|
||||
"verifyPasskey": "Подтвердить пароль",
|
||||
"recoverAccount": "Восстановить аккаунт",
|
||||
"enterRecoveryKeyHint": "Введите свой ключ восстановления",
|
||||
"recover": "Восстановить",
|
||||
@@ -146,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "Неверный QR-код",
|
||||
"noRecoveryKeyTitle": "Нет ключа восстановления?",
|
||||
"enterEmailHint": "Введите свою почту",
|
||||
"invalidEmailTitle": "Неверный адрес электронной почты",
|
||||
@@ -190,6 +203,8 @@
|
||||
"saveKey": "Сохранить ключ",
|
||||
"save": "Сохранить",
|
||||
"send": "Отправить",
|
||||
"saveOrSendDescription": "Вы хотите сохранить это в хранилище (папку загрузок по умолчанию) или отправить в другие приложения?",
|
||||
"saveOnlyDescription": "Вы хотите сохранить это в хранилище (по умолчанию папка загрузок)?",
|
||||
"back": "Вернуться",
|
||||
"createAccount": "Создать аккаунт",
|
||||
"passwordStrength": "Мощность пароля: {passwordStrengthValue}",
|
||||
@@ -337,6 +352,7 @@
|
||||
"deleteCodeAuthMessage": "Аутентификация для удаления кода",
|
||||
"showQRAuthMessage": "Аутентификация для отображения QR-кода",
|
||||
"confirmAccountDeleteTitle": "Подтвердить удаление аккаунта",
|
||||
"confirmAccountDeleteMessage": "Эта учетная запись связана с другими приложениями Ente, если вы ими пользуетесь.\n\nЗагруженные вами данные во всех приложениях ente будут запланированы к удалению, а ваша учетная запись будет удалена без возможности восстановления.",
|
||||
"androidBiometricHint": "Подтвердите личность",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -397,12 +413,28 @@
|
||||
"doNotSignOut": "Не выходить",
|
||||
"hearUsWhereTitle": "Как вы узнали о Ente? (необязательно)",
|
||||
"hearUsExplanation": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения",
|
||||
"recoveryKeySaved": "Ключ восстановления сохранён в папке Загрузки!",
|
||||
"waitingForBrowserRequest": "Ожидание запроса браузера...",
|
||||
"waitingForVerification": "Ожидание подтверждения...",
|
||||
"passkey": "Ключ",
|
||||
"developerSettingsWarning": "Вы уверены, что хотите изменить настройки разработчика?",
|
||||
"developerSettings": "Настройки разработчика",
|
||||
"serverEndpoint": "Конечная точка сервера",
|
||||
"invalidEndpoint": "Неверная конечная точка",
|
||||
"invalidEndpointMessage": "Извините, введенная вами конечная точка неверна. Пожалуйста, введите корректную конечную точку и повторите попытку.",
|
||||
"endpointUpdatedMessage": "Конечная точка успешно обновлена",
|
||||
"customEndpoint": "Подключено к {endpoint}"
|
||||
"customEndpoint": "Подключено к {endpoint}",
|
||||
"pinText": "Прикрепить",
|
||||
"unpinText": "Открепить",
|
||||
"pinnedCodeMessage": "{code} прикреплен",
|
||||
"unpinnedCodeMessage": "{code} откреплен",
|
||||
"tags": "Метки",
|
||||
"createNewTag": "Создать новую метку",
|
||||
"tag": "Метка",
|
||||
"create": "Создать",
|
||||
"editTag": "Изменить метку",
|
||||
"deleteTagTitle": "Удалить метку?",
|
||||
"deleteTagMessage": "Вы уверены, что хотите удалить эту метку? Это действие необратимо.",
|
||||
"somethingWentWrongParsingCode": "Мы не смогли разобрать коды {x}.",
|
||||
"updateNotAvailable": "Обновление недоступно"
|
||||
}
|
||||
@@ -61,6 +61,7 @@
|
||||
"welcomeBack": "Välkommen tillbaka!",
|
||||
"changePassword": "Ändra lösenord",
|
||||
"importCodes": "Importera koder",
|
||||
"exportCodes": "Exportera koder",
|
||||
"cancel": "Avbryt",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
@@ -76,6 +77,7 @@
|
||||
"scan": "Skanna",
|
||||
"twoFactorAuthTitle": "Tvåfaktorsautentisering",
|
||||
"enterRecoveryKeyHint": "Ange din återställningsnyckel",
|
||||
"invalidQRCode": "Ogiltig QR-kod",
|
||||
"noRecoveryKeyTitle": "Ingen återställningsnyckel?",
|
||||
"enterEmailHint": "Ange din e-postadress",
|
||||
"invalidEmailTitle": "Ogiltig e-postadress",
|
||||
@@ -143,6 +145,8 @@
|
||||
},
|
||||
"pendingSyncs": "Varning",
|
||||
"activeSessions": "Aktiva sessioner",
|
||||
"incorrectCode": "Felaktig kod",
|
||||
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
|
||||
"enterPassword": "Ange lösenord",
|
||||
"export": "Exportera",
|
||||
"singIn": "Logga in",
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "Yayınlayan",
|
||||
"codeSecretKeyHint": "Gizli Anahtar",
|
||||
"codeAccountHint": "Hesap (ornek@domain.com)",
|
||||
"codeTagHint": "Etiket",
|
||||
"accountKeyType": "Anahtar türü",
|
||||
"sessionExpired": "Oturum süresi doldu",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -46,7 +48,7 @@
|
||||
},
|
||||
"copyEmailAction": "E-postayı Kopyala",
|
||||
"exportLogsAction": "Günlüğü dışa aktar",
|
||||
"reportABug": "Bir hata bildir",
|
||||
"reportABug": "Hata bildirin",
|
||||
"crashAndErrorReporting": "Çökme ve hata bildirimi",
|
||||
"reportBug": "Hata bildir",
|
||||
"emailUsMessage": "Lütfen bize {email} adresinden ulaşın",
|
||||
@@ -77,12 +79,14 @@
|
||||
"data": "Veri",
|
||||
"importCodes": "Kodu içe aktar",
|
||||
"importTypePlainText": "Salt metin",
|
||||
"importTypeEnteEncrypted": "Ente Şifreli dışa aktarma",
|
||||
"passwordForDecryptingExport": "Dışa aktarımın şifresini çözmek için parola",
|
||||
"passwordEmptyError": "Şifre boş olamaz",
|
||||
"importFromApp": "Kodları {appName} uygulamasından içe aktarın",
|
||||
"importGoogleAuthGuide": "\"Hesapları Aktar\" seçeneğini kullanarak hesaplarınızı Google Authenticator'dan bir QR koduna aktarın. Ardından başka bir cihaz kullanarak QR kodunu tarayın.\n\nİpucu: QR kodunun fotoğrafını çekmek için dizüstü bilgisayarınızın kamerasını kullanabilirsiniz.",
|
||||
"importSelectJsonFile": "JSON dosyasını seçin",
|
||||
"importSelectAppExport": "{appName} dışarı aktarma dosyasını seçin",
|
||||
"importEnteEncGuide": "Ente'den dışa aktarılan şifrelenmiş JSON dosyasını seçin",
|
||||
"importRaivoGuide": "Raivo'nun ayarlarında \"OTP'leri Zip arşivine aktar\" seçeneğini kullanın.\n\nZip dosyasını çıkarın ve JSON dosyasını içe aktarın.",
|
||||
"importBitwardenGuide": "Bitwarden Tools içindeki \"Kasayı dışa aktar\" seçeneğini kullanın ve şifrelenmemiş JSON dosyasını içe aktarın.",
|
||||
"importAegisGuide": "Aegis'in Ayarlarında \"Kasayı dışa aktar\" seçeneğini kullanın.\n\nKasanız şifrelenmişse, kasanın şifresini çözmek için kasa parolasını girmeniz gerekecektir.",
|
||||
@@ -112,18 +116,22 @@
|
||||
"copied": "Kopyalandı",
|
||||
"pleaseTryAgain": "Lütfen tekrar deneyin",
|
||||
"existingUser": "Mevcut kullanıcı",
|
||||
"newUser": "Ente'de Yeni",
|
||||
"delete": "Sil",
|
||||
"enterYourPasswordHint": "Parolanızı girin",
|
||||
"forgotPassword": "Şifremi unuttum",
|
||||
"oops": "Hay aksi",
|
||||
"suggestFeatures": "Özellik önerin",
|
||||
"faq": "SSS",
|
||||
"faq_q_1": "Kimlik doğrulayıcı ne kadar güvenli?",
|
||||
"faq_a_1": "Auth aracılığıyla yedeklediğiniz tüm kodlar uçtan uca şifrelenmiş olarak saklanır. Böylece kodlarınıza yalnızca siz erişebilirsiniz. Uygulamalarımız açık kaynaklıdır ve şifrelememiz dış denetimden geçmiştir.",
|
||||
"faq_q_2": "Kodlarıma masaüstünden erişebilir miyim?",
|
||||
"faq_a_2": "Kodlarınıza internet üzerinden @ auth.ente.io adresinden erişebilirsiniz.",
|
||||
"faq_q_3": "Kodları nasıl silebilirim?",
|
||||
"faq_a_3": "Bir kodu, o öğenin üzerinde sola kaydırarak silebilirsiniz.",
|
||||
"faq_q_4": "Bu projeye nasıl destek olabilirim?",
|
||||
"faq_a_4": "Fotoğraflar uygulamamıza @ ente.io abone olarak bu projenin geliştirilmesine destek olabilirsiniz.",
|
||||
"faq_q_5": "Auth'ta FaceID kilidini nasıl etkinleştirebilirim",
|
||||
"faq_a_5": "FaceID kilidini Ayarlar → Güvenlik → Kilit Ekranı altında etkinleştirebilirsiniz.",
|
||||
"somethingWentWrongMessage": "Bir şeyler ters gitti, lütfen tekrar deneyin",
|
||||
"leaveFamily": "Aile planından ayrıl",
|
||||
@@ -137,6 +145,8 @@
|
||||
"enterCodeHint": "Kimlik doğrulayıcı uygulamanızdaki 6 haneli doğrulama kodunu girin",
|
||||
"lostDeviceTitle": "Cihazınızı mı kaybettiniz?",
|
||||
"twoFactorAuthTitle": "İki faktörlü kimlik doğrulama",
|
||||
"passkeyAuthTitle": "Geçiş anahtarı doğrulaması",
|
||||
"verifyPasskey": "Geçiş anahtarını doğrula",
|
||||
"recoverAccount": "Hesap kurtarma",
|
||||
"enterRecoveryKeyHint": "Kurtarma anahtarınızı girin",
|
||||
"recover": "Kurtar",
|
||||
@@ -148,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "Geçersiz QR kodu",
|
||||
"noRecoveryKeyTitle": "Kurtarma anahtarınız yok mu?",
|
||||
"enterEmailHint": "E-posta adresinizi girin",
|
||||
"invalidEmailTitle": "Geçersiz e-posta adresi",
|
||||
@@ -190,6 +201,10 @@
|
||||
"recoveryKeySaveDescription": "Biz bu anahtarı saklamıyoruz, lütfen. bu 24 kelimelik anahtarı güvenli bir yerde saklayın.",
|
||||
"doThisLater": "Bunu daha sonra yap",
|
||||
"saveKey": "Anahtarı kaydet",
|
||||
"save": "Kaydet",
|
||||
"send": "Gönder",
|
||||
"saveOrSendDescription": "Bunu belleğinize mi kaydedeceksiniz (İndirilenler klasörü varsayılandır) yoksa diğer uygulamalara mı göndereceksiniz?",
|
||||
"saveOnlyDescription": "Bunu belleğinize kaydetmek ister misiniz? (İndirilenler klasörü varsayılandır)",
|
||||
"back": "Geri",
|
||||
"createAccount": "Hesap oluştur",
|
||||
"passwordStrength": "Şifre gücü: {passwordStrengthValue}",
|
||||
@@ -337,6 +352,7 @@
|
||||
"deleteCodeAuthMessage": "Kodu silmek için doğrulama yapın",
|
||||
"showQRAuthMessage": "QR kodunu göstermek için doğrulama yapın",
|
||||
"confirmAccountDeleteTitle": "Hesap silme işlemini onayla",
|
||||
"confirmAccountDeleteMessage": "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır.\n\nTüm Ente uygulamalarına yüklediğiniz veriler ve hesabınız kalıcı olarak silinecektir.",
|
||||
"androidBiometricHint": "Kimliği doğrula",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -396,5 +412,29 @@
|
||||
"signOutOtherDevices": "Diğer cihazlardan çıkış yap",
|
||||
"doNotSignOut": "Çıkış yapma",
|
||||
"hearUsWhereTitle": "Ente'yi nereden duydunuz? (opsiyonel)",
|
||||
"hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!"
|
||||
"hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!",
|
||||
"recoveryKeySaved": "Kurtarma anahtarı İndirilenler klasörüne kaydedildi!",
|
||||
"waitingForBrowserRequest": "Tarayıcı isteği bekleniyor...",
|
||||
"waitingForVerification": "Doğrulama bekleniyor...",
|
||||
"passkey": "Geçiş anahtarı",
|
||||
"developerSettingsWarning": "Geliştirici ayarlarını değiştirmekten emin misiniz?",
|
||||
"developerSettings": "Geliştirici ayarları",
|
||||
"serverEndpoint": "Sunucu uç noktası",
|
||||
"invalidEndpoint": "Geçersiz uç nokta",
|
||||
"invalidEndpointMessage": "Üzgünüz, girdiğiniz uç nokta geçersiz. Lütfen geçerli bir uç nokta girin ve tekrar deneyin.",
|
||||
"endpointUpdatedMessage": "Uç nokta başarıyla güncellendi",
|
||||
"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",
|
||||
"create": "Oluştur",
|
||||
"editTag": "Etiketi düzenle",
|
||||
"deleteTagTitle": "Etiket silinsin mi?",
|
||||
"deleteTagMessage": "Bu etiketi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"somethingWentWrongParsingCode": "{x} kodu ayrıştıramadık.",
|
||||
"updateNotAvailable": "Güncelleme mevcut değil"
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
"codeIssuerHint": "发行人",
|
||||
"codeSecretKeyHint": "私钥",
|
||||
"codeAccountHint": "账户 (you@domain.com)",
|
||||
"codeTagHint": "标签",
|
||||
"accountKeyType": "密钥类型",
|
||||
"sessionExpired": "会话已过期",
|
||||
"@sessionExpired": {
|
||||
"description": "Title of the dialog when the users current session is invalid/expired"
|
||||
@@ -156,6 +158,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "二维码无效",
|
||||
"noRecoveryKeyTitle": "没有恢复密钥吗?",
|
||||
"enterEmailHint": "请输入您的电子邮件地址",
|
||||
"invalidEmailTitle": "无效的电子邮件地址",
|
||||
@@ -420,5 +423,18 @@
|
||||
"invalidEndpoint": "端点无效",
|
||||
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
|
||||
"endpointUpdatedMessage": "端点更新成功",
|
||||
"customEndpoint": "已连接至 {endpoint}"
|
||||
"customEndpoint": "已连接至 {endpoint}",
|
||||
"pinText": "置顶",
|
||||
"unpinText": "取消置顶",
|
||||
"pinnedCodeMessage": "{code} 已被置顶",
|
||||
"unpinnedCodeMessage": "{code} 已被取消置顶",
|
||||
"tags": "标签",
|
||||
"createNewTag": "创建新标签",
|
||||
"tag": "标签",
|
||||
"create": "创建",
|
||||
"editTag": "编辑标签",
|
||||
"deleteTagTitle": "要删除标签吗?",
|
||||
"deleteTagMessage": "您确定要删除此标签吗?此操作是不可逆的。",
|
||||
"somethingWentWrongParsingCode": "我们无法解析 {x} 代码。",
|
||||
"updateNotAvailable": "更新不可用"
|
||||
}
|
||||
@@ -125,10 +125,10 @@ class Code {
|
||||
final issuer = _getIssuer(uri);
|
||||
|
||||
try {
|
||||
return Code(
|
||||
final code = Code(
|
||||
_getAccount(uri),
|
||||
issuer,
|
||||
_getDigits(uri, issuer),
|
||||
_getDigits(uri),
|
||||
_getPeriod(uri),
|
||||
getSanitizedSecret(uri.queryParameters['secret']!),
|
||||
_getAlgorithm(uri),
|
||||
@@ -137,6 +137,7 @@ class Code {
|
||||
rawData,
|
||||
display: CodeDisplay.fromUri(uri) ?? CodeDisplay(),
|
||||
);
|
||||
return code;
|
||||
} catch (e) {
|
||||
// if account name contains # without encoding,
|
||||
// rest of the url are treated as url fragment
|
||||
@@ -174,12 +175,11 @@ class Code {
|
||||
}
|
||||
|
||||
String toOTPAuthUrlFormat() {
|
||||
final uri = Uri.parse(rawData);
|
||||
final uri = Uri.parse(rawData.replaceAll("#", '%23'));
|
||||
final query = {...uri.queryParameters};
|
||||
query["codeDisplay"] = jsonEncode(display.toJson());
|
||||
|
||||
final newUri = uri.replace(queryParameters: query);
|
||||
|
||||
return jsonEncode(newUri.toString());
|
||||
}
|
||||
|
||||
@@ -201,11 +201,11 @@ class Code {
|
||||
}
|
||||
}
|
||||
|
||||
static int _getDigits(Uri uri, String issuer) {
|
||||
static int _getDigits(Uri uri) {
|
||||
try {
|
||||
return int.parse(uri.queryParameters['digits']!);
|
||||
} catch (e) {
|
||||
if (issuer.toLowerCase() == "steam") {
|
||||
if (uri.host == "steam") {
|
||||
return steamDigits;
|
||||
}
|
||||
return defaultDigits;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Used to store the display settings of a code.
|
||||
class CodeDisplay {
|
||||
@@ -54,13 +55,34 @@ class CodeDisplay {
|
||||
);
|
||||
}
|
||||
|
||||
static CodeDisplay? fromUri(Uri uri) {
|
||||
/// Converts the [CodeDisplay] to a json object.
|
||||
/// When [safeParsing] is true, the json will be parsed safely.
|
||||
/// If we fail to parse the json, we will return an empty [CodeDisplay].
|
||||
static CodeDisplay? fromUri(Uri uri, {bool safeParsing = false}) {
|
||||
if (!uri.queryParameters.containsKey("codeDisplay")) return null;
|
||||
final String codeDisplay =
|
||||
uri.queryParameters['codeDisplay']!.replaceAll('%2C', ',');
|
||||
final decodedDisplay = jsonDecode(codeDisplay);
|
||||
return _parseCodeDisplayJson(codeDisplay, safeParsing);
|
||||
}
|
||||
|
||||
return CodeDisplay.fromJson(decodedDisplay);
|
||||
static CodeDisplay _parseCodeDisplayJson(String json, bool safeParsing) {
|
||||
try {
|
||||
final decodedDisplay = jsonDecode(json);
|
||||
return CodeDisplay.fromJson(decodedDisplay);
|
||||
} catch (e, s) {
|
||||
Logger("CodeDisplay")
|
||||
.severe("Could not parse code display from json", e, s);
|
||||
// (ng/prateek) Handle the case where we have fragment in the rawDataUrl
|
||||
if (!json.endsWith("}") && json.contains("}#")) {
|
||||
Logger("CodeDisplay").warning("ignoring code display as it's invalid");
|
||||
return CodeDisplay();
|
||||
}
|
||||
if (safeParsing) {
|
||||
return CodeDisplay();
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
||||
@@ -240,7 +240,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final account = _accountController.text.trim();
|
||||
final issuer = _issuerController.text.trim();
|
||||
final secret = _secretController.text.trim().replaceAll(' ', '');
|
||||
final isStreamCode = issuer.toLowerCase() == "steam";
|
||||
final isStreamCode = issuer.toLowerCase() == "steam" || issuer.toLowerCase().contains('steampowered.com');
|
||||
if (widget.code != null && widget.code!.secret != secret) {
|
||||
ButtonResult? result = await showChoiceActionSheet(
|
||||
context,
|
||||
|
||||
@@ -41,9 +41,9 @@ class CodeStore {
|
||||
} else {
|
||||
code = Code.fromExportJson(decodeJson);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
code = Code.withError(e, entity.rawData);
|
||||
_logger.severe("Could not parse code", code.err);
|
||||
_logger.severe("Could not parse code", e, s);
|
||||
}
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
|
||||
@@ -48,7 +48,6 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
late bool _shouldShowLargeIcon;
|
||||
late bool _hideCode;
|
||||
bool isMaskingEnabled = false;
|
||||
late final colorScheme = getEnteColorScheme(context);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -78,6 +77,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
if (isMaskingEnabled != PreferenceService.instance.shouldHideCodes()) {
|
||||
isMaskingEnabled = PreferenceService.instance.shouldHideCodes();
|
||||
_hideCode = isMaskingEnabled;
|
||||
@@ -91,6 +91,100 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
_isInitialized = true;
|
||||
}
|
||||
final l10n = context.l10n;
|
||||
|
||||
Widget getCardContents(AppLocalizations l10n) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (widget.code.isPinned)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.pinnedBgColor,
|
||||
),
|
||||
size: const Size(39, 39),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgress(
|
||||
period: widget.code.period,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_getTopRow(),
|
||||
const SizedBox(height: 4),
|
||||
_getBottomRow(l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.code.isPinned) ...[
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 6, top: 6),
|
||||
child: SvgPicture.asset("assets/svg/pin-card.svg"),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget clippedCard(AppLocalizations l10n) {
|
||||
return Container(
|
||||
height: 132,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
boxShadow:
|
||||
widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
onDoubleTap: isMaskingEnabled
|
||||
? () {
|
||||
setState(
|
||||
() {
|
||||
_hideCode = !_hideCode;
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
child: getCardContents(l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Builder(
|
||||
@@ -126,7 +220,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
],
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
child: _clippedCard(l10n),
|
||||
child: clippedCard(l10n),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,7 +310,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => _clippedCard(l10n),
|
||||
builder: (context) => clippedCard(l10n),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -224,98 +318,6 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _clippedCard(AppLocalizations l10n) {
|
||||
return Container(
|
||||
height: 132,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
boxShadow: widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
onDoubleTap: isMaskingEnabled
|
||||
? () {
|
||||
setState(
|
||||
() {
|
||||
_hideCode = !_hideCode;
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
child: _getCardContents(l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getCardContents(AppLocalizations l10n) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (widget.code.isPinned)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.pinnedBgColor,
|
||||
),
|
||||
size: const Size(39, 39),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgress(
|
||||
period: widget.code.period,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_getTopRow(),
|
||||
const SizedBox(height: 4),
|
||||
_getBottomRow(l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.code.isPinned) ...[
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 6, top: 6),
|
||||
child: SvgPicture.asset("assets/svg/pin-card.svg"),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBottomRow(AppLocalizations l10n) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
@@ -585,7 +587,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
String _getFormattedCode(String code) {
|
||||
if (_hideCode) {
|
||||
// replace all digits with •
|
||||
code = code.replaceAll(RegExp(r'\d'), '•');
|
||||
code = code.replaceAll(RegExp(r'\S'), '•');
|
||||
}
|
||||
if (code.length == 6) {
|
||||
return "${code.substring(0, 3)} ${code.substring(3, 6)}";
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:otp/otp.dart' as otp;
|
||||
import 'package:steam_totp/steam_totp.dart';
|
||||
|
||||
String getOTP(Code code) {
|
||||
if (code.issuer.toLowerCase() == 'steam') {
|
||||
if (code.type == Type.steam) {
|
||||
return _getSteamCode(code);
|
||||
}
|
||||
if (code.type == Type.hotp) {
|
||||
@@ -39,7 +39,7 @@ String _getSteamCode(Code code, [bool isNext = false]) {
|
||||
}
|
||||
|
||||
String getNextTotp(Code code) {
|
||||
if (code.issuer.toLowerCase() == 'steam') {
|
||||
if (code.type == Type.steam) {
|
||||
return _getSteamCode(code, true);
|
||||
}
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 3.0.3+303
|
||||
version: 3.0.4+304
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -113,3 +113,23 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte,
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(decryptedData), decryptedData, nil
|
||||
}
|
||||
|
||||
func DecryptChaChaBase64Auth(data string, key []byte, nonce string) (string, []byte, error) {
|
||||
// Decode data from base64
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
// safe to log the encrypted data
|
||||
return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err)
|
||||
}
|
||||
// Decode nonce from base64
|
||||
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid nonce: %v", err)
|
||||
}
|
||||
// Decrypt data
|
||||
decryptedData, err := decryptChaCha20poly1305V2(dataBytes, key, nonceBytes)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to decrypt data: %v", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(decryptedData), decryptedData, nil
|
||||
}
|
||||
|
||||
@@ -88,6 +88,23 @@ func decryptChaCha20poly1305(data []byte, key []byte, nonce []byte) ([]byte, err
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// decryptChaCha20poly1305V2 is used only to decrypt Ente Auth data. Ente Auth use new version of LibSodium.
|
||||
// In that version, the final tag value is 0x0 instead of TagFinal.
|
||||
func decryptChaCha20poly1305V2(data []byte, key []byte, nonce []byte) ([]byte, error) {
|
||||
decryptor, err := NewDecryptor(key, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoded, tag, err := decryptor.Pull(data)
|
||||
if tag != TagFinal && tag != TagMessage {
|
||||
return nil, errors.New("invalid tag")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
//func SecretBoxOpenLibSodium(c []byte, n []byte, k []byte) ([]byte, error) {
|
||||
// var cp sodium.Bytes = c
|
||||
// res, err := cp.SecretBoxOpen(sodium.SecretBoxNonce{Bytes: n}, sodium.SecretBoxKey{Bytes: k})
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var AppVersion = "0.1.13"
|
||||
var AppVersion = "0.1.14"
|
||||
|
||||
func main() {
|
||||
cliDBPath, err := GetCLIConfigPath()
|
||||
|
||||
@@ -55,7 +55,7 @@ func DecryptExport(inputPath string, outputPath string) error {
|
||||
return fmt.Errorf("error deriving key: %v", err)
|
||||
}
|
||||
|
||||
_, decryptedData, err := eCrypto.DecryptChaChaBase64(export.EncryptedData, key, export.EncryptionNonce)
|
||||
_, decryptedData, err := eCrypto.DecryptChaChaBase64Auth(export.EncryptedData, key, export.EncryptionNonce)
|
||||
if err != nil {
|
||||
fmt.Printf("\nerror decrypting data %v", err)
|
||||
fmt.Println("\nPlease check your password and try again")
|
||||
|
||||
36
desktop/.github/workflows/desktop-release.yml
vendored
@@ -5,12 +5,19 @@ name: "Release"
|
||||
# For more details, see `docs/release.md` in ente-io/ente.
|
||||
|
||||
on:
|
||||
# Trigger manually or `gh workflow run desktop-release.yml`.
|
||||
# Trigger manually or `gh workflow run desktop-release.yml --source=foo`.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source:
|
||||
description: "Branch (ente-io/ente) to build"
|
||||
type: string
|
||||
schedule:
|
||||
# Run everyday at ~8:00 AM IST (except Sundays).
|
||||
# See: [Note: Run workflow every 24 hours]
|
||||
#
|
||||
- cron: "45 2 * * 1-6"
|
||||
push:
|
||||
# Run when a tag matching the pattern "v*"" is pushed.
|
||||
#
|
||||
# See: [Note: Testing release workflows that are triggered by tags].
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
@@ -30,9 +37,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Checkout the desktop/rc branch from the source repository.
|
||||
# If triggered by a tag, checkout photosd-$tag from the source
|
||||
# repository. Otherwise checkout $source (default: "main").
|
||||
repository: ente-io/ente
|
||||
ref: desktop/rc
|
||||
ref:
|
||||
"${{ startsWith(github.ref, 'refs/tags/v') &&
|
||||
format('photosd-{0}', github.ref_name) || ( inputs.source
|
||||
|| 'main' ) }}"
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node
|
||||
@@ -64,10 +75,8 @@ jobs:
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# create a (draft) release after building. Otherwise upload
|
||||
# assets to the existing draft named after the version.
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
# Passes `--publish always` to electron-builder
|
||||
release: true
|
||||
|
||||
mac_certs: ${{ secrets.MAC_CERTS }}
|
||||
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
@@ -77,4 +86,13 @@ jobs:
|
||||
APPLE_APP_SPECIFIC_PASSWORD:
|
||||
${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
# Default is "draft", but since our nightly builds update
|
||||
# existing pre-releases, set this to "prerelease".
|
||||
EP_PRE_RELEASE: true
|
||||
# By default, electron-builder does not update releases that
|
||||
# were more than 2 hours ago. Override this to allow us to
|
||||
# continually update our nightly pre-releases.
|
||||
EP_GH_IGNORE_TIME: true
|
||||
# Workaround recommended in
|
||||
# https://github.com/electron-userland/electron-builder/issues/3179
|
||||
USE_HARD_LINKS: false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.7.0 (Unreleased)
|
||||
## v1.7.0
|
||||
|
||||
v1.7 is a major rewrite to improve the security of our app. In particular, the
|
||||
UI and the native parts of the app now run isolated from each other and
|
||||
|
||||
@@ -1,65 +1,94 @@
|
||||
## Releases
|
||||
|
||||
Conceptually, the release is straightforward: We trigger a GitHub workflow that
|
||||
creates a draft release with artifacts built. When ready, we publish that
|
||||
release. The download links on our website, and existing apps already check the
|
||||
latest GitHub release and update accordingly.
|
||||
Conceptually, the release is straightforward:
|
||||
|
||||
The complication comes by the fact that electron-builder's auto updaterr (the
|
||||
1. We trigger a GitHub workflow that creates a (pre-)release with the build.
|
||||
|
||||
2. When ready, we make that release the latest.
|
||||
|
||||
3. The download links on our website, and existing apps already check the
|
||||
latest GitHub release and update automatically.
|
||||
|
||||
The complication comes by the fact that electron-builder's auto updater (the
|
||||
mechanism that we use for auto updates) doesn't work with monorepos. So we need
|
||||
to keep a separate (non-mono) repository just for doing releases.
|
||||
to keep a separate repository just for holding the releases.
|
||||
|
||||
- Source code lives here, in [ente-io/ente](https://github.com/ente-io/ente).
|
||||
|
||||
- Releases are done from
|
||||
[ente-io/photos-desktop](https://github.com/ente-io/photos-desktop).
|
||||
|
||||
## Workflow - Release Candidates
|
||||
## Workflow - Release candidates
|
||||
|
||||
Leading up to the release, we can make one or more draft releases that are not
|
||||
intended to be published, but serve as test release candidates.
|
||||
Nightly RC builds of `main` are published by a scheduled workflow automatically.
|
||||
If needed, these builds can also be manually updated, and the branch of the
|
||||
source repository to build (default "main") also specified:
|
||||
|
||||
The workflow for making such "rc" builds is:
|
||||
```sh
|
||||
gh workflow run desktop-release.yml --source=<branch>
|
||||
```
|
||||
|
||||
1. Update `package.json` in the source repo to use version `1.x.x-rc`. Create a
|
||||
new draft release in the release repo with title `1.x.x-rc`. In the tag
|
||||
input enter `v1.x.x-rc` and select the option to "create a new tag on
|
||||
publish".
|
||||
|
||||
2. Push code to the `desktop/rc` branch in the source repo.
|
||||
|
||||
3. Trigger the GitHub action in the release repo
|
||||
|
||||
```sh
|
||||
gh workflow run desktop-release.yml
|
||||
```
|
||||
|
||||
We can do steps 2 and 3 multiple times: each time it'll just update the
|
||||
artifacts attached to the same draft.
|
||||
Each such workflow run will update the artifacts attached to the same
|
||||
(pre-existing) pre-release.
|
||||
|
||||
## Workflow - Release
|
||||
|
||||
1. Update source repo to set version `1.x.x` in `package.json` and finialize
|
||||
the CHANGELOG.
|
||||
1. Update source repo to set version `1.x.x` in `package.json` and finalize the
|
||||
CHANGELOG.
|
||||
|
||||
2. Push code to the `desktop/rc` branch in the source repo.
|
||||
2. Merge PR then tag the merge commit on `main` in the source repo:
|
||||
|
||||
3. In the release repo
|
||||
```sh
|
||||
git tag photosd-v1.x.x
|
||||
git push origin photosd-v1.x.x
|
||||
```
|
||||
|
||||
3. In the release repo:
|
||||
|
||||
```sh
|
||||
./.github/trigger-release.sh v1.x.x
|
||||
```
|
||||
|
||||
4. If the build is successful, tag `desktop/rc` in the source repo.
|
||||
This'll trigger the workflow and create a new pre-release. We can edit this to
|
||||
add the release notes, convert it to a release. Once it is marked as latest, the
|
||||
release goes live.
|
||||
|
||||
We are done at this point, and can now create a new pre-release to host
|
||||
subsequent nightly builds.
|
||||
|
||||
1. Update `package.json` in the source repo to use version `1.x.x-rc`, and
|
||||
merge these changes into `main`.
|
||||
|
||||
2. In the release repo:
|
||||
|
||||
```sh
|
||||
# Assuming we're on desktop/rc that just got build
|
||||
|
||||
git tag photosd-v1.x.x
|
||||
git push origin photosd-v1.x.x
|
||||
git tag 1.x.x-rc
|
||||
git push origin 1.x.x-rc
|
||||
```
|
||||
|
||||
## Post build
|
||||
3. Once the workflow finishes and the pre-release is created, edit its
|
||||
description to "Nightly builds".
|
||||
|
||||
4. Delete the pre-release for the previous (already released) version.
|
||||
|
||||
## Workflow - Extra pre-releases
|
||||
|
||||
To create extra one off pre-releases in addition to the nightly `1.x.x-rc` ones,
|
||||
|
||||
1. In your branch in the source repository, set the version in `package.json`
|
||||
to something different, say `1.x.x-my-test`.
|
||||
|
||||
2. Create a new pre-release in the release repo with title `1.x.x-test`. In the
|
||||
tag input enter `v1.x.x-test` and select the option to "create a new tag on
|
||||
publish".
|
||||
|
||||
3. Trigger the workflow in the release repo:
|
||||
|
||||
```sh
|
||||
gh workflow run desktop-release.yml --source=my-branch
|
||||
```
|
||||
|
||||
## Details
|
||||
|
||||
The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts
|
||||
defined in the `build` value in `package.json`.
|
||||
@@ -87,8 +116,3 @@ everything is automated:
|
||||
now their maintainers automatically bump the SHA, version number and the
|
||||
(derived from the version) URL in the formula when their tools notice a new
|
||||
release on our GitHub.
|
||||
|
||||
We can also publish the draft releases by checking the "pre-release" option.
|
||||
Such releases don't cause any of the channels (our website, or the desktop app
|
||||
auto updater, or brew) to be notified, instead these are useful for giving links
|
||||
to pre-release builds to customers.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ente",
|
||||
"version": "1.7.0-rc",
|
||||
"version": "1.7.1-rc",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"repository": "github:ente-io/photos-desktop",
|
||||
@@ -30,7 +30,7 @@
|
||||
"compare-versions": "^6.1",
|
||||
"electron-log": "^5.1",
|
||||
"electron-store": "^8.2",
|
||||
"electron-updater": "^6.1",
|
||||
"electron-updater": "^6.2",
|
||||
"ffmpeg-static": "^5.2",
|
||||
"html-entities": "^2.5",
|
||||
"jpeg-js": "^0.4",
|
||||
|
||||
@@ -322,6 +322,13 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||
* once most people have upgraded to newer versions.
|
||||
*/
|
||||
const deleteLegacyDiskCacheDirIfExists = async () => {
|
||||
const removeIfExists = async (dirPath: string) => {
|
||||
if (existsSync(dirPath)) {
|
||||
log.info(`Removing legacy disk cache from ${dirPath}`);
|
||||
await fs.rm(dirPath, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
// [Note: Getting the cache path]
|
||||
//
|
||||
// The existing code was passing "cache" as a parameter to getPath.
|
||||
@@ -338,9 +345,18 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
|
||||
//
|
||||
// @ts-expect-error "cache" works but is not part of the public API.
|
||||
const cacheDir = path.join(app.getPath("cache"), "ente");
|
||||
if (existsSync(cacheDir)) {
|
||||
log.info(`Removing legacy disk cache from ${cacheDir}`);
|
||||
await fs.rm(cacheDir, { recursive: true });
|
||||
if (process.platform == "win32") {
|
||||
// On Windows the cache dir is the same as the app data (!). So deleting
|
||||
// the ente subfolder of the cache dir is equivalent to deleting the
|
||||
// user data dir.
|
||||
//
|
||||
// Obviously, that's not good. So instead of Windows we explicitly
|
||||
// delete the named cache directories.
|
||||
await removeIfExists(path.join(cacheDir, "thumbs"));
|
||||
await removeIfExists(path.join(cacheDir, "files"));
|
||||
await removeIfExists(path.join(cacheDir, "face-crops"));
|
||||
} else {
|
||||
await removeIfExists(cacheDir);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,19 +12,22 @@ import { watchReset } from "./watch";
|
||||
* See: [Note: Do not throw during logout].
|
||||
*/
|
||||
export const logout = (watcher: FSWatcher) => {
|
||||
const ignoreError = (label: string, e: unknown) =>
|
||||
log.error(`Ignoring error during logout (${label})`, e);
|
||||
|
||||
try {
|
||||
watchReset(watcher);
|
||||
} catch (e) {
|
||||
log.error("Ignoring error during logout (FS watch)", e);
|
||||
ignoreError("FS watch", e);
|
||||
}
|
||||
try {
|
||||
clearConvertToMP4Results();
|
||||
} catch (e) {
|
||||
log.error("Ignoring error during logout (convert-to-mp4)", e);
|
||||
ignoreError("convert-to-mp4", e);
|
||||
}
|
||||
try {
|
||||
clearStores();
|
||||
} catch (e) {
|
||||
log.error("Ignoring error during logout (native stores)", e);
|
||||
ignoreError("native stores", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,7 +106,7 @@ const handleRead = async (path: string) => {
|
||||
res.headers.set("Content-Length", `${fileSize}`);
|
||||
|
||||
// Add the file's last modified time (as epoch milliseconds).
|
||||
const mtimeMs = stat.mtimeMs;
|
||||
const mtimeMs = stat.mtime.getTime();
|
||||
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
|
||||
}
|
||||
return res;
|
||||
@@ -132,6 +132,13 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
// Close the zip handle when the underlying stream closes.
|
||||
stream.on("end", () => void zip.close());
|
||||
|
||||
// While it is documented that entry.time is the modification time,
|
||||
// the units are not mentioned. By seeing the source code, we can
|
||||
// verify that it is indeed epoch milliseconds. See `parseZipTime`
|
||||
// in the node-stream-zip source,
|
||||
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
|
||||
const modifiedMs = entry.time;
|
||||
|
||||
return new Response(webReadableStream, {
|
||||
headers: {
|
||||
// We don't know the exact type, but it doesn't really matter, just
|
||||
@@ -139,12 +146,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
// doesn't tinker with it thinking of it as text.
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": `${entry.size}`,
|
||||
// While it is documented that entry.time is the modification time,
|
||||
// the units are not mentioned. By seeing the source code, we can
|
||||
// verify that it is indeed epoch milliseconds. See `parseZipTime`
|
||||
// in the node-stream-zip source,
|
||||
// https://github.com/antelle/node-stream-zip/blob/master/node_stream_zip.js
|
||||
"X-Last-Modified-Ms": `${entry.time}`,
|
||||
"X-Last-Modified-Ms": `${modifiedMs}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -743,10 +743,10 @@ buffer@^5.1.0, buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@9.2.3:
|
||||
version "9.2.3"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c"
|
||||
integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==
|
||||
builder-util-runtime@9.2.4:
|
||||
version "9.2.4"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a"
|
||||
integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
@@ -1251,12 +1251,12 @@ electron-store@^8.2:
|
||||
conf "^10.2.0"
|
||||
type-fest "^2.17.0"
|
||||
|
||||
electron-updater@^6.1:
|
||||
version "6.1.8"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8"
|
||||
integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ==
|
||||
electron-updater@^6.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9"
|
||||
integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==
|
||||
dependencies:
|
||||
builder-util-runtime "9.2.3"
|
||||
builder-util-runtime "9.2.4"
|
||||
fs-extra "^10.1.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
|
||||
@@ -163,6 +163,10 @@ export const sidebar = [
|
||||
text: "From Authy",
|
||||
link: "/auth/migration-guides/authy/",
|
||||
},
|
||||
{
|
||||
text: "From Steam",
|
||||
link: "/auth/migration-guides/steam/",
|
||||
},
|
||||
{
|
||||
text: "Exporting your data",
|
||||
link: "/auth/migration-guides/export",
|
||||
|
||||
@@ -7,4 +7,5 @@ description:
|
||||
# Migrating to/from Ente Auth
|
||||
|
||||
- [Migrating from Authy](authy/)
|
||||
- [Importing codes from Steam](steam/)
|
||||
- [Exporting your data out of Ente Auth](export)
|
||||
|
||||
79
docs/docs/auth/migration-guides/steam/index.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Migrating from Steam Authenticator
|
||||
description: Guide for importing from Steam Authenticator to Ente Auth
|
||||
---
|
||||
|
||||
# Migrating from Steam Authenticator
|
||||
|
||||
A guide written by an ente.io lover
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Steam Authenticator code is only supported after auth-v3.0.3, check the app's
|
||||
> version number before migration.
|
||||
|
||||
One way to migrate is to
|
||||
[use this tool by dyc3](https://github.com/dyc3/steamguard-cli/releases/latest)
|
||||
to simplify the process and skip directly to generating a qr code to Ente
|
||||
Authenticator.
|
||||
|
||||
## Download/Install steamguard-cli
|
||||
|
||||
### Windows
|
||||
|
||||
1. Download `steamguard.exe` from the [releases page][releases].
|
||||
2. Place `steamguard.exe` in a folder of your choice. For this example, we will
|
||||
use `%USERPROFILE%\Desktop`.
|
||||
3. Open Powershell or Command Prompt. The prompt should be at `%USERPROFILE%`
|
||||
(eg. `C:\Users\<username>`).
|
||||
4. Use `cd` to change directory into the folder where you placed
|
||||
`steamguard.exe`. For this example, it would be `cd Desktop`.
|
||||
5. You should now be able to run `steamguard.exe` by typing
|
||||
`.\steamguard.exe --help` and pressing enter.
|
||||
|
||||
### Linux
|
||||
|
||||
#### Ubuntu/Debian
|
||||
|
||||
1. Download the `.deb` from the [releases page][releases].
|
||||
2. Open a terminal and run this to install it:
|
||||
|
||||
```bash
|
||||
sudo dpkg -i ./steamguard-cli_<version>_amd64.deb
|
||||
```
|
||||
|
||||
#### Other Linux
|
||||
|
||||
1. Download `steamguard` from the [releases page][releases]
|
||||
2. Make it executable, and move `steamguard` to `/usr/local/bin` or any other
|
||||
directory in your `$PATH`.
|
||||
|
||||
```bash
|
||||
chmod +x ./steamguard
|
||||
sudo mv ./steamguard /usr/local/bin
|
||||
```
|
||||
|
||||
3. You should now be able to run `steamguard` by typing `steamguard --help` and
|
||||
pressing enter.
|
||||
|
||||
## Login to Steam account
|
||||
|
||||
Set up a new account with steamguard-cli
|
||||
|
||||
```bash
|
||||
steamguard setup # set up a new account with steamguard-cli
|
||||
```
|
||||
|
||||
## Generate & importing QR codes
|
||||
|
||||
steamguard-cli can then generate a QR code for your 2FA secret.
|
||||
|
||||
```bash
|
||||
steamguard qr # print QR code for the first account in your maFiles
|
||||
steamguard -u <account name> qr # print QR code for a specific account
|
||||
```
|
||||
|
||||
Open Ente Auth, press the '+' button, select `Scan a QR code`, and scan the qr
|
||||
code.
|
||||
|
||||
You should now have your steam code inside Ente Auth
|
||||
@@ -78,3 +78,23 @@ To summarize:
|
||||
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or
|
||||
some such IP/hostname that accessible from both where you are running the Ente
|
||||
clients (e.g. the mobile app) and also from within the Docker compose cluster.
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but
|
||||
uploads are still failing, it could be a credentials or permissions issue. A
|
||||
telltale sign of this is that in the museum logs you can see `403 Forbidden`
|
||||
errors about it not able to find the size of a file even though the
|
||||
corresponding object exists in the S3 bucket.
|
||||
|
||||
To fix these, you should ensure the following:
|
||||
|
||||
1. The bucket CORS rules do not allow museum to access these objects.
|
||||
|
||||
> For uploading files from the browser, you will need to currently set
|
||||
> allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package"
|
||||
> headers configuration too.
|
||||
> [Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
|
||||
|
||||
2. The credentials are not being picked up (you might be setting the correct
|
||||
creds, but not in the place where museum picks them from).
|
||||
|
||||
@@ -37,7 +37,7 @@ endpoint:
|
||||
(Another
|
||||
[example](https://github.com/ente-io/ente/blob/main/cli/config.yaml.example))
|
||||
|
||||
## Web appps and Photos desktop app
|
||||
## Web apps and Photos desktop app
|
||||
|
||||
You will need to build the app from source and use the
|
||||
`NEXT_PUBLIC_ENTE_ENDPOINT` environment variable to tell it which server to
|
||||
|
||||
@@ -46,7 +46,7 @@ You can alternatively install the build from PlayStore or F-Droid.
|
||||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter v3.19.3](https://flutter.dev/docs/get-started/install).
|
||||
1. [Install Flutter v3.22.0](https://flutter.dev/docs/get-started/install).
|
||||
|
||||
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||
|
||||
|
||||
@@ -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.
|
||||
Si vous recherchez une alternative respectueuse de votre 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 Android, iOS, Web et Ordinateur, et vos photos seront synchronisées de manière transparente entre tous vos appareils chiffrée de bout en bout (e2ee).
|
||||
Nous avons des applications pour Android, iOS, Web et Ordinateur, et vos photos seront synchronisées de manière transparente entre tous vos appareils avec une méthode de 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.
|
||||
|
||||
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.
|
||||
Vos données chiffrées sont stockées dans 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
|
||||
- Sauvegardes en qualité originale, car chaque pixel est important
|
||||
- Abonnement 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
|
||||
- Favoriser, cacher et revivre vos souvenirs, car ils sont précieux
|
||||
- Favoris, cacher et revivre vos souvenirs, car ils sont précieux
|
||||
- Importation en un clic depuis Google, Apple, votre disque dur et plus encore
|
||||
- Thème sombre, parce que vos photos y sont jolies
|
||||
- 2FA, 3FA, authentification biométrique
|
||||
- et beaucoup de choses encore !
|
||||
|
||||
💲 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'abonnements gratuits à vie, car il est important pour nous de rester pérenne 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.
|
||||
@@ -427,7 +427,7 @@ SPEC CHECKSUMS:
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892
|
||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
|
||||
@@ -35,10 +35,10 @@ import 'package:photos/services/sync_service.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:photos/utils/validator_util.dart';
|
||||
import "package:photos/utils/wakelock_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import "package:tuple/tuple.dart";
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
class Configuration {
|
||||
Configuration._privateConstructor();
|
||||
@@ -585,7 +585,7 @@ class Configuration {
|
||||
|
||||
Future<void> setShouldKeepDeviceAwake(bool value) async {
|
||||
await _preferences.setBool(keyShouldKeepDeviceAwake, value);
|
||||
await WakelockPlus.toggle(enable: value);
|
||||
await EnteWakeLock.toggle(enable: value);
|
||||
}
|
||||
|
||||
Future<void> setShouldBackupVideos(bool value) async {
|
||||
|
||||
@@ -69,6 +69,8 @@ const galleryGridSpacing = 2.0;
|
||||
|
||||
const kSearchSectionLimit = 9;
|
||||
|
||||
const maxPickAssetLimit = 50;
|
||||
|
||||
const iOSGroupID = "group.io.ente.frame.SlideshowWidget";
|
||||
|
||||
const blackThumbnailBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB'
|
||||
|
||||
@@ -22,61 +22,55 @@ extension DeviceFiles on FilesDB {
|
||||
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore,
|
||||
}) async {
|
||||
debugPrint("Inserting missing PathIDToLocalIDMapping");
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
final parameterSets = <List<Object?>>[];
|
||||
int batchCounter = 0;
|
||||
for (MapEntry e in mappingToAdd.entries) {
|
||||
final String pathID = e.key;
|
||||
for (String localID in e.value) {
|
||||
parameterSets.add([localID, pathID]);
|
||||
batchCounter++;
|
||||
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
await _insertBatch(parameterSets, conflictAlgorithm);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.insert(
|
||||
"device_files",
|
||||
{
|
||||
"id": localID,
|
||||
"path_id": pathID,
|
||||
},
|
||||
conflictAlgorithm: conflictAlgorithm,
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
await _insertBatch(parameterSets, conflictAlgorithm);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
|
||||
Future<void> deletePathIDToLocalIDMapping(
|
||||
Map<String, Set<String>> mappingsToRemove,
|
||||
) async {
|
||||
debugPrint("removing PathIDToLocalIDMapping");
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
final parameterSets = <List<Object?>>[];
|
||||
int batchCounter = 0;
|
||||
for (MapEntry e in mappingsToRemove.entries) {
|
||||
final String pathID = e.key;
|
||||
|
||||
for (String localID in e.value) {
|
||||
parameterSets.add([localID, pathID]);
|
||||
batchCounter++;
|
||||
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
await _deleteBatch(parameterSets);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.delete(
|
||||
"device_files",
|
||||
where: 'id = ? AND path_id = ?',
|
||||
whereArgs: [localID, pathID],
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
await _deleteBatch(parameterSets);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
|
||||
Future<Map<String, int>> getDevicePathIDToImportedFileCount() async {
|
||||
try {
|
||||
final db = await database;
|
||||
final rows = await db.rawQuery(
|
||||
final db = await sqliteAsyncDB;
|
||||
final rows = await db.getAll(
|
||||
'''
|
||||
SELECT count(*) as count, path_id
|
||||
FROM device_files
|
||||
@@ -96,8 +90,8 @@ extension DeviceFiles on FilesDB {
|
||||
|
||||
Future<Map<String, Set<String>>> getDevicePathIDToLocalIDMap() async {
|
||||
try {
|
||||
final db = await database;
|
||||
final rows = await db.rawQuery(
|
||||
final db = await sqliteAsyncDB;
|
||||
final rows = await db.getAll(
|
||||
''' SELECT id, path_id FROM device_files; ''',
|
||||
);
|
||||
final result = <String, Set<String>>{};
|
||||
@@ -116,8 +110,8 @@ extension DeviceFiles on FilesDB {
|
||||
}
|
||||
|
||||
Future<Set<String>> getDevicePathIDs() async {
|
||||
final Database db = await database;
|
||||
final rows = await db.rawQuery(
|
||||
final db = await sqliteAsyncDB;
|
||||
final rows = await db.getAll(
|
||||
'''
|
||||
SELECT id FROM device_collections
|
||||
''',
|
||||
@@ -133,34 +127,42 @@ extension DeviceFiles on FilesDB {
|
||||
List<LocalPathAsset> localPathAssets, {
|
||||
bool shouldAutoBackup = false,
|
||||
}) async {
|
||||
final Database db = await database;
|
||||
final db = await sqliteAsyncDB;
|
||||
final Map<String, Set<String>> pathIDToLocalIDsMap = {};
|
||||
try {
|
||||
final batch = db.batch();
|
||||
final Set<String> existingPathIds = await getDevicePathIDs();
|
||||
final parameterSetsForUpdate = <List<Object?>>[];
|
||||
final parameterSetsForInsert = <List<Object?>>[];
|
||||
for (LocalPathAsset localPathAsset in localPathAssets) {
|
||||
if (localPathAsset.localIDs.isNotEmpty) {
|
||||
pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs;
|
||||
}
|
||||
if (existingPathIds.contains(localPathAsset.pathID)) {
|
||||
batch.rawUpdate(
|
||||
"UPDATE device_collections SET name = ? where id = "
|
||||
"?",
|
||||
[localPathAsset.pathName, localPathAsset.pathID],
|
||||
);
|
||||
parameterSetsForUpdate
|
||||
.add([localPathAsset.pathName, localPathAsset.pathID]);
|
||||
} else if (localPathAsset.localIDs.isNotEmpty) {
|
||||
batch.insert(
|
||||
"device_collections",
|
||||
{
|
||||
"id": localPathAsset.pathID,
|
||||
"name": localPathAsset.pathName,
|
||||
"should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
);
|
||||
parameterSetsForInsert.add([
|
||||
localPathAsset.pathID,
|
||||
localPathAsset.pathName,
|
||||
shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse,
|
||||
]);
|
||||
}
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
|
||||
await db.executeBatch(
|
||||
'''
|
||||
INSERT OR IGNORE INTO device_collections (id, name, should_backup) VALUES (?, ?, ?);
|
||||
''',
|
||||
parameterSetsForInsert,
|
||||
);
|
||||
|
||||
await db.executeBatch(
|
||||
'''
|
||||
UPDATE device_collections SET name = ? WHERE id = ?;
|
||||
''',
|
||||
parameterSetsForUpdate,
|
||||
);
|
||||
|
||||
// add the mappings for localIDs
|
||||
if (pathIDToLocalIDsMap.isNotEmpty) {
|
||||
await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap);
|
||||
@@ -177,7 +179,7 @@ extension DeviceFiles on FilesDB {
|
||||
}) async {
|
||||
bool hasUpdated = false;
|
||||
try {
|
||||
final Database db = await database;
|
||||
final db = await sqliteAsyncDB;
|
||||
final Set<String> existingPathIds = await getDevicePathIDs();
|
||||
for (Tuple2<AssetPathEntity, String> tup in devicePathInfo) {
|
||||
final AssetPathEntity pathEntity = tup.item1;
|
||||
@@ -185,35 +187,42 @@ extension DeviceFiles on FilesDB {
|
||||
final String localID = tup.item2;
|
||||
final bool shouldUpdate = existingPathIds.contains(pathEntity.id);
|
||||
if (shouldUpdate) {
|
||||
final rowUpdated = await db.rawUpdate(
|
||||
"UPDATE device_collections SET name = ?, cover_id = ?, count"
|
||||
" = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)",
|
||||
[
|
||||
pathEntity.name,
|
||||
localID,
|
||||
assetCount,
|
||||
pathEntity.id,
|
||||
pathEntity.name,
|
||||
localID,
|
||||
assetCount,
|
||||
],
|
||||
);
|
||||
final rowUpdated = await db.writeTransaction((tx) async {
|
||||
await tx.execute(
|
||||
"UPDATE device_collections SET name = ?, cover_id = ?, count"
|
||||
" = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)",
|
||||
[
|
||||
pathEntity.name,
|
||||
localID,
|
||||
assetCount,
|
||||
pathEntity.id,
|
||||
pathEntity.name,
|
||||
localID,
|
||||
assetCount,
|
||||
],
|
||||
);
|
||||
final result = await tx.get("SELECT changes();");
|
||||
return result["changes()"] as int;
|
||||
});
|
||||
|
||||
if (rowUpdated > 0) {
|
||||
_logger.fine("Updated $rowUpdated rows for ${pathEntity.name}");
|
||||
hasUpdated = true;
|
||||
}
|
||||
} else {
|
||||
hasUpdated = true;
|
||||
await db.insert(
|
||||
"device_collections",
|
||||
{
|
||||
"id": pathEntity.id,
|
||||
"name": pathEntity.name,
|
||||
"count": assetCount,
|
||||
"cover_id": localID,
|
||||
"should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse,
|
||||
},
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
await db.execute(
|
||||
'''
|
||||
INSERT INTO device_collections (id, name, count, cover_id, should_backup)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
''',
|
||||
[
|
||||
pathEntity.id,
|
||||
pathEntity.name,
|
||||
assetCount,
|
||||
localID,
|
||||
shouldBackup ? _sqlBoolTrue : _sqlBoolFalse,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -231,15 +240,17 @@ extension DeviceFiles on FilesDB {
|
||||
// feature, where we delete files which are backed up. Deleting such
|
||||
// entries here result in us losing out on the information that
|
||||
// those folders were marked for automatic backup.
|
||||
await db.delete(
|
||||
"device_collections",
|
||||
where: 'id = ? and should_backup = $_sqlBoolFalse ',
|
||||
whereArgs: [pathID],
|
||||
await db.execute(
|
||||
'''
|
||||
DELETE FROM device_collections WHERE id = ? AND should_backup = $_sqlBoolFalse;
|
||||
''',
|
||||
[pathID],
|
||||
);
|
||||
await db.delete(
|
||||
"device_files",
|
||||
where: 'path_id = ?',
|
||||
whereArgs: [pathID],
|
||||
await db.execute(
|
||||
'''
|
||||
DELETE FROM device_files WHERE path_id = ?;
|
||||
''',
|
||||
[pathID],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -253,8 +264,8 @@ extension DeviceFiles on FilesDB {
|
||||
// getDeviceSyncCollectionIDs returns the collectionIDs for the
|
||||
// deviceCollections which are marked for auto-backup
|
||||
Future<Set<int>> getDeviceSyncCollectionIDs() async {
|
||||
final Database db = await database;
|
||||
final rows = await db.rawQuery(
|
||||
final db = await sqliteAsyncDB;
|
||||
final rows = await db.getAll(
|
||||
'''
|
||||
SELECT collection_id FROM device_collections where should_backup =
|
||||
$_sqlBoolTrue
|
||||
@@ -268,40 +279,47 @@ extension DeviceFiles on FilesDB {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> updateDevicePathSyncStatus(Map<String, bool> syncStatus) async {
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
Future<void> updateDevicePathSyncStatus(
|
||||
Map<String, bool> syncStatus,
|
||||
) async {
|
||||
final db = await sqliteAsyncDB;
|
||||
int batchCounter = 0;
|
||||
final parameterSets = <List<Object?>>[];
|
||||
for (MapEntry e in syncStatus.entries) {
|
||||
final String pathID = e.key;
|
||||
parameterSets.add([e.value ? _sqlBoolTrue : _sqlBoolFalse, pathID]);
|
||||
batchCounter++;
|
||||
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
await db.executeBatch(
|
||||
'''
|
||||
UPDATE device_collections SET should_backup = ? WHERE id = ?;
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.update(
|
||||
"device_collections",
|
||||
{
|
||||
"should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [pathID],
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
|
||||
await db.executeBatch(
|
||||
'''
|
||||
UPDATE device_collections SET should_backup = ? WHERE id = ?;
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDeviceCollection(
|
||||
String pathID,
|
||||
int collectionID,
|
||||
) async {
|
||||
final db = await database;
|
||||
await db.update(
|
||||
"device_collections",
|
||||
{"collection_id": collectionID},
|
||||
where: 'id = ?',
|
||||
whereArgs: [pathID],
|
||||
final db = await sqliteAsyncDB;
|
||||
await db.execute(
|
||||
'''
|
||||
UPDATE device_collections SET collection_id = ? WHERE id = ?;
|
||||
''',
|
||||
[collectionID, pathID],
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -314,7 +332,7 @@ extension DeviceFiles on FilesDB {
|
||||
int? limit,
|
||||
bool? asc,
|
||||
}) async {
|
||||
final db = await database;
|
||||
final db = await sqliteAsyncDB;
|
||||
final order = (asc ?? false ? 'ASC' : 'DESC');
|
||||
final String rawQuery = '''
|
||||
SELECT *
|
||||
@@ -329,7 +347,7 @@ extension DeviceFiles on FilesDB {
|
||||
ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order
|
||||
''' +
|
||||
(limit != null ? ' limit $limit;' : ';');
|
||||
final results = await db.rawQuery(rawQuery);
|
||||
final results = await db.getAll(rawQuery);
|
||||
final files = convertToFiles(results);
|
||||
final dedupe = deduplicateByLocalID(files);
|
||||
return FileLoadResult(dedupe, files.length == limit);
|
||||
@@ -339,7 +357,7 @@ extension DeviceFiles on FilesDB {
|
||||
String pathID,
|
||||
int ownerID,
|
||||
) async {
|
||||
final db = await database;
|
||||
final db = await sqliteAsyncDB;
|
||||
const String rawQuery = '''
|
||||
SELECT ${FilesDB.columnLocalID}, ${FilesDB.columnUploadedFileID},
|
||||
${FilesDB.columnFileSize}
|
||||
@@ -351,7 +369,7 @@ extension DeviceFiles on FilesDB {
|
||||
${FilesDB.columnLocalID} IN
|
||||
(SELECT id FROM device_files where path_id = ?)
|
||||
''';
|
||||
final results = await db.rawQuery(rawQuery, [ownerID, pathID]);
|
||||
final results = await db.getAll(rawQuery, [ownerID, pathID]);
|
||||
final localIDs = <String>{};
|
||||
final uploadedIDs = <int>{};
|
||||
int localSize = 0;
|
||||
@@ -375,17 +393,17 @@ extension DeviceFiles on FilesDB {
|
||||
"$includeCoverThumbnail",
|
||||
);
|
||||
try {
|
||||
final db = await database;
|
||||
final db = await sqliteAsyncDB;
|
||||
final coverFiles = <EnteFile>[];
|
||||
if (includeCoverThumbnail) {
|
||||
final fileRows = await db.rawQuery(
|
||||
final fileRows = await db.getAll(
|
||||
'''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id;
|
||||
''',
|
||||
);
|
||||
final files = convertToFiles(fileRows);
|
||||
coverFiles.addAll(files);
|
||||
}
|
||||
final deviceCollectionRows = await db.rawQuery(
|
||||
final deviceCollectionRows = await db.getAll(
|
||||
'''SELECT * from device_collections''',
|
||||
);
|
||||
final List<DeviceCollection> deviceCollections = [];
|
||||
@@ -433,8 +451,8 @@ extension DeviceFiles on FilesDB {
|
||||
|
||||
Future<EnteFile?> getDeviceCollectionThumbnail(String pathID) async {
|
||||
debugPrint("Call fallback method to get potential thumbnail");
|
||||
final db = await database;
|
||||
final fileRows = await db.rawQuery(
|
||||
final db = await sqliteAsyncDB;
|
||||
final fileRows = await db.getAll(
|
||||
'''SELECT * FROM FILES f JOIN device_files df on f.local_id = df.id
|
||||
and df.path_id= ? order by f.creation_time DESC limit 1;
|
||||
''',
|
||||
@@ -447,4 +465,28 @@ extension DeviceFiles on FilesDB {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _insertBatch(
|
||||
List<List<Object?>> parameterSets,
|
||||
ConflictAlgorithm conflictAlgorithm,
|
||||
) async {
|
||||
final db = await sqliteAsyncDB;
|
||||
await db.executeBatch(
|
||||
'''
|
||||
INSERT OR ${conflictAlgorithm.name.toUpperCase()}
|
||||
INTO device_files (id, path_id) VALUES (?, ?);
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteBatch(List<List<Object?>> parameterSets) async {
|
||||
final db = await sqliteAsyncDB;
|
||||
await db.executeBatch(
|
||||
'''
|
||||
DELETE FROM device_files WHERE id = ? AND path_id = ?;
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class EmbeddingsDB {
|
||||
|
||||
Future<void> clearTable() async {
|
||||
final db = await _database;
|
||||
await db.execute('DELETE * FROM $tableName');
|
||||
await db.execute('DELETE FROM $tableName');
|
||||
}
|
||||
|
||||
Future<List<Embedding>> getAll(Model model) async {
|
||||
|
||||
@@ -10,53 +10,78 @@ extension EntitiesDB on FilesDB {
|
||||
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace,
|
||||
}) async {
|
||||
debugPrint("entitiesDB: upsertEntities ${data.length} entities");
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
final db = await sqliteAsyncDB;
|
||||
final parameterSets = <List<Object?>>[];
|
||||
int batchCounter = 0;
|
||||
for (LocalEntityData e in data) {
|
||||
parameterSets.add([
|
||||
e.id,
|
||||
e.type.name,
|
||||
e.ownerID,
|
||||
e.data,
|
||||
e.updatedAt,
|
||||
]);
|
||||
batchCounter++;
|
||||
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
await db.executeBatch(
|
||||
'''
|
||||
INSERT OR ${conflictAlgorithm.name.toUpperCase()}
|
||||
INTO entities (id, type, ownerID, data, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.insert(
|
||||
"entities",
|
||||
e.toJson(),
|
||||
conflictAlgorithm: conflictAlgorithm,
|
||||
);
|
||||
batchCounter++;
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
await db.executeBatch(
|
||||
'''
|
||||
INSERT OR ${conflictAlgorithm.name.toUpperCase()}
|
||||
INTO entities (id, type, ownerID, data, updatedAt)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteEntities(
|
||||
List<String> ids,
|
||||
) async {
|
||||
final db = await database;
|
||||
var batch = db.batch();
|
||||
final db = await sqliteAsyncDB;
|
||||
final parameterSets = <List<Object?>>[];
|
||||
int batchCounter = 0;
|
||||
for (String id in ids) {
|
||||
if (batchCounter == 400) {
|
||||
await batch.commit(noResult: true);
|
||||
batch = db.batch();
|
||||
batchCounter = 0;
|
||||
}
|
||||
batch.delete(
|
||||
"entities",
|
||||
where: "id = ?",
|
||||
whereArgs: [id],
|
||||
parameterSets.add(
|
||||
[id],
|
||||
);
|
||||
batchCounter++;
|
||||
|
||||
if (batchCounter == 400) {
|
||||
await db.executeBatch(
|
||||
'''
|
||||
DELETE FROM entities WHERE id = ?
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
parameterSets.clear();
|
||||
batchCounter = 0;
|
||||
}
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
await db.executeBatch(
|
||||
'''
|
||||
DELETE FROM entities WHERE id = ?
|
||||
''',
|
||||
parameterSets,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<LocalEntityData>> getEntities(EntityType type) async {
|
||||
final db = await database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
"entities",
|
||||
where: "type = ?",
|
||||
whereArgs: [type.typeToString()],
|
||||
final db = await sqliteAsyncDB;
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(
|
||||
'SELECT * FROM entities WHERE type = ?',
|
||||
[type.name],
|
||||
);
|
||||
return List.generate(maps.length, (i) {
|
||||
return LocalEntityData.fromJson(maps[i]);
|
||||
@@ -64,11 +89,10 @@ extension EntitiesDB on FilesDB {
|
||||
}
|
||||
|
||||
Future<LocalEntityData?> getEntity(EntityType type, String id) async {
|
||||
final db = await database;
|
||||
final List<Map<String, dynamic>> maps = await db.query(
|
||||
"entities",
|
||||
where: "type = ? AND id = ?",
|
||||
whereArgs: [type.typeToString(), id],
|
||||
final db = await sqliteAsyncDB;
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(
|
||||
'SELECT * FROM entities WHERE type = ? AND id = ?',
|
||||
[type.name, id],
|
||||
);
|
||||
if (maps.isEmpty) {
|
||||
return null;
|
||||
|
||||
@@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart";
|
||||
import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart';
|
||||
import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
|
||||
import "package:photos/utils/ml_util.dart";
|
||||
import 'package:sqlite_async/sqlite_async.dart';
|
||||
|
||||
/// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`.
|
||||
@@ -27,12 +29,21 @@ class FaceMLDataDB {
|
||||
static final Logger _logger = Logger("FaceMLDataDB");
|
||||
|
||||
static const _databaseName = "ente.face_ml_db.db";
|
||||
static const _databaseVersion = 1;
|
||||
// static const _databaseVersion = 1;
|
||||
|
||||
FaceMLDataDB._privateConstructor();
|
||||
|
||||
static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor();
|
||||
|
||||
static final _migrationScripts = [
|
||||
createFacesTable,
|
||||
createFaceClustersTable,
|
||||
createClusterPersonTable,
|
||||
createClusterSummaryTable,
|
||||
createNotPersonFeedbackTable,
|
||||
fcClusterIDIndex,
|
||||
];
|
||||
|
||||
// only have a single app-wide reference to the database
|
||||
static Future<SqliteDatabase>? _sqliteAsyncDBFuture;
|
||||
|
||||
@@ -48,23 +59,42 @@ class FaceMLDataDB {
|
||||
_logger.info("Opening sqlite_async access: DB path " + databaseDirectory);
|
||||
final asyncDBConnection =
|
||||
SqliteDatabase(path: databaseDirectory, maxReaders: 2);
|
||||
await _onCreate(asyncDBConnection);
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_logger.info("FaceMLDataDB: Starting migration");
|
||||
await _migrate(asyncDBConnection);
|
||||
_logger.info(
|
||||
"FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
|
||||
);
|
||||
stopwatch.stop();
|
||||
|
||||
return asyncDBConnection;
|
||||
}
|
||||
|
||||
Future<void> _onCreate(SqliteDatabase asyncDBConnection) async {
|
||||
final migrations = SqliteMigrations()
|
||||
..add(
|
||||
SqliteMigration(_databaseVersion, (tx) async {
|
||||
await tx.execute(createFacesTable);
|
||||
await tx.execute(createFaceClustersTable);
|
||||
await tx.execute(createClusterPersonTable);
|
||||
await tx.execute(createClusterSummaryTable);
|
||||
await tx.execute(createNotPersonFeedbackTable);
|
||||
await tx.execute(fcClusterIDIndex);
|
||||
}),
|
||||
Future<void> _migrate(
|
||||
SqliteDatabase database,
|
||||
) async {
|
||||
final result = await database.execute('PRAGMA user_version');
|
||||
final currentVersion = result[0]['user_version'] as int;
|
||||
final toVersion = _migrationScripts.length;
|
||||
|
||||
if (currentVersion < toVersion) {
|
||||
_logger.info("Migrating database from $currentVersion to $toVersion");
|
||||
await database.writeTransaction((tx) async {
|
||||
for (int i = currentVersion + 1; i <= toVersion; i++) {
|
||||
try {
|
||||
await tx.execute(_migrationScripts[i - 1]);
|
||||
} catch (e) {
|
||||
_logger.severe("Error running migration script index ${i - 1}", e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
await tx.execute('PRAGMA user_version = $toVersion');
|
||||
});
|
||||
} else if (currentVersion > toVersion) {
|
||||
throw AssertionError(
|
||||
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
|
||||
);
|
||||
await migrations.migrate(asyncDBConnection);
|
||||
}
|
||||
}
|
||||
|
||||
// bulkInsertFaces inserts the faces in the database in batches of 1000.
|
||||
@@ -199,10 +229,10 @@ class FaceMLDataDB {
|
||||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
}
|
||||
|
||||
Future<Iterable<Uint8List>> getFaceEmbeddingsForCluster(
|
||||
@@ -255,7 +285,7 @@ class FaceMLDataDB {
|
||||
final List<int> fileId = [recentFileID];
|
||||
int? avatarFileId;
|
||||
if (avatarFaceId != null) {
|
||||
avatarFileId = int.tryParse(avatarFaceId.split('_')[0]);
|
||||
avatarFileId = tryGetFileIdFromFaceId(avatarFaceId);
|
||||
if (avatarFileId != null) {
|
||||
fileId.add(avatarFileId);
|
||||
}
|
||||
@@ -407,8 +437,10 @@ class FaceMLDataDB {
|
||||
final personID = map[personIdColumn] as String;
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceID = map[fcFaceId] as String;
|
||||
result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {})
|
||||
..add(faceID);
|
||||
result
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(clusterID, () => {})
|
||||
.add(faceID);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -482,8 +514,7 @@ class FaceMLDataDB {
|
||||
for (final map in maps) {
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceID = map[fcFaceId] as String;
|
||||
final x = faceID.split('_').first;
|
||||
final fileID = int.parse(x);
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
@@ -671,19 +702,55 @@ class FaceMLDataDB {
|
||||
return maps.first['count'] as int;
|
||||
}
|
||||
|
||||
Future<int> getClusteredFaceCount() async {
|
||||
Future<int> getClusteredOrFacelessFileCount() async {
|
||||
final db = await instance.asyncDB;
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(
|
||||
'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable',
|
||||
final List<Map<String, dynamic>> clustered = await db.getAll(
|
||||
'SELECT $fcFaceId FROM $faceClustersTable',
|
||||
);
|
||||
return maps.first['count'] as int;
|
||||
final Set<int> clusteredFileIDs = {};
|
||||
for (final map in clustered) {
|
||||
final int fileID = getFileIdFromFaceId(map[fcFaceId] as String);
|
||||
clusteredFileIDs.add(fileID);
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> badFacesFiles = await db.getAll(
|
||||
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore <= $kMinimumQualityFaceScore OR $faceBlur <= $kLaplacianHardThreshold',
|
||||
);
|
||||
final Set<int> badFileIDs = {};
|
||||
for (final map in badFacesFiles) {
|
||||
badFileIDs.add(map[fileIDColumn] as int);
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> goodFacesFiles = await db.getAll(
|
||||
'SELECT DISTINCT $fileIDColumn FROM $facesTable WHERE $faceScore > $kMinimumQualityFaceScore AND $faceBlur > $kLaplacianHardThreshold',
|
||||
);
|
||||
final Set<int> goodFileIDs = {};
|
||||
for (final map in goodFacesFiles) {
|
||||
goodFileIDs.add(map[fileIDColumn] as int);
|
||||
}
|
||||
final trulyFacelessFiles = badFileIDs.difference(goodFileIDs);
|
||||
return clusteredFileIDs.length + trulyFacelessFiles.length;
|
||||
}
|
||||
|
||||
Future<double> getClusteredToTotalFacesRatio() async {
|
||||
final int totalFaces = await getTotalFaceCount();
|
||||
final int clusteredFaces = await getClusteredFaceCount();
|
||||
Future<double> getClusteredToIndexableFilesRatio() async {
|
||||
final int indexableFiles = (await getIndexableFileIDs()).length;
|
||||
final int clusteredFiles = await getClusteredOrFacelessFileCount();
|
||||
|
||||
return clusteredFaces / totalFaces;
|
||||
return clusteredFiles / indexableFiles;
|
||||
}
|
||||
|
||||
Future<int> getUnclusteredFaceCount() async {
|
||||
final db = await instance.asyncDB;
|
||||
const String query = '''
|
||||
SELECT f.$faceIDColumn
|
||||
FROM $facesTable f
|
||||
LEFT JOIN $faceClustersTable fc ON f.$faceIDColumn = fc.$fcFaceId
|
||||
WHERE f.$faceScore > $kMinimumQualityFaceScore
|
||||
AND f.$faceBlur > $kLaplacianHardThreshold
|
||||
AND fc.$fcFaceId IS NULL
|
||||
''';
|
||||
final List<Map<String, dynamic>> maps = await db.getAll(query);
|
||||
return maps.length;
|
||||
}
|
||||
|
||||
Future<int> getBlurryFaceCount([
|
||||
@@ -701,7 +768,7 @@ class FaceMLDataDB {
|
||||
try {
|
||||
final db = await instance.asyncDB;
|
||||
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
} catch (e, s) {
|
||||
@@ -801,7 +868,7 @@ class FaceMLDataDB {
|
||||
for (final map in maps) {
|
||||
final clusterID = map[clusterIDColumn] as int;
|
||||
final String faceID = map[fcFaceId] as String;
|
||||
final fileID = int.parse(faceID.split('_').first);
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
@@ -820,8 +887,8 @@ class FaceMLDataDB {
|
||||
final Map<int, Set<int>> result = {};
|
||||
for (final map in maps) {
|
||||
final clusterID = map[fcClusterID] as int;
|
||||
final faceId = map[fcFaceId] as String;
|
||||
final fileID = int.parse(faceId.split("_").first);
|
||||
final faceID = map[fcFaceId] as String;
|
||||
final fileID = getFileIdFromFaceId(faceID);
|
||||
result[fileID] = (result[fileID] ?? {})..add(clusterID);
|
||||
}
|
||||
return result;
|
||||
@@ -912,17 +979,16 @@ class FaceMLDataDB {
|
||||
if (faces) {
|
||||
await db.execute(deleteFacesTable);
|
||||
await db.execute(createFacesTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
await db.execute(createFaceClustersTable);
|
||||
await db.execute(fcClusterIDIndex);
|
||||
}
|
||||
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(dropClusterSummaryTable);
|
||||
await db.execute(dropFaceClustersTable);
|
||||
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterSummaryTable);
|
||||
await db.execute(deleteFaceClustersTable);
|
||||
|
||||
await db.execute(createClusterPersonTable);
|
||||
await db.execute(createNotPersonFeedbackTable);
|
||||
await db.execute(createClusterSummaryTable);
|
||||
@@ -939,9 +1005,8 @@ class FaceMLDataDB {
|
||||
final db = await instance.asyncDB;
|
||||
|
||||
// Drop the tables
|
||||
await db.execute(deletePersonTable);
|
||||
await db.execute(dropClusterPersonTable);
|
||||
await db.execute(dropNotPersonFeedbackTable);
|
||||
await db.execute(deleteClusterPersonTable);
|
||||
await db.execute(deleteNotPersonFeedbackTable);
|
||||
|
||||
// Recreate the tables
|
||||
await db.execute(createClusterPersonTable);
|
||||
@@ -970,7 +1035,7 @@ class FaceMLDataDB {
|
||||
final Map<String, int> faceIDToClusterID = {};
|
||||
for (final row in faceIdsResult) {
|
||||
final faceID = row[fcFaceId] as String;
|
||||
if (fileIds.contains(faceID.split('_').first)) {
|
||||
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||
maxClusterID += 1;
|
||||
faceIDToClusterID[faceID] = maxClusterID;
|
||||
}
|
||||
@@ -996,7 +1061,7 @@ class FaceMLDataDB {
|
||||
final Map<String, int> faceIDToClusterID = {};
|
||||
for (final row in faceIdsResult) {
|
||||
final faceID = row[fcFaceId] as String;
|
||||
if (fileIds.contains(faceID.split('_').first)) {
|
||||
if (fileIds.contains(getFileIdFromFaceId(faceID))) {
|
||||
maxClusterID += 1;
|
||||
faceIDToClusterID[faceID] = maxClusterID;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
|
||||
);
|
||||
''';
|
||||
|
||||
const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable';
|
||||
const deleteFacesTable = 'DELETE FROM $facesTable';
|
||||
// End of Faces Table Fields & Schema Queries
|
||||
|
||||
//##region Face Clusters Table Fields & Schema Queries
|
||||
@@ -48,15 +48,9 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable (
|
||||
// -- Creating a non-unique index on clusterID for query optimization
|
||||
const fcClusterIDIndex =
|
||||
'''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);''';
|
||||
const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable';
|
||||
const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable';
|
||||
//##endregion
|
||||
|
||||
// People Table Fields & Schema Queries
|
||||
const personTable = 'person';
|
||||
|
||||
const deletePersonTable = 'DROP TABLE IF EXISTS $personTable';
|
||||
//End People Table Fields & Schema Queries
|
||||
|
||||
// Clusters Table Fields & Schema Queries
|
||||
const clusterPersonTable = 'cluster_person';
|
||||
const personIdColumn = 'person_id';
|
||||
@@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable (
|
||||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable';
|
||||
const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
||||
/// Cluster Summary Table Fields & Schema Queries
|
||||
@@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable (
|
||||
);
|
||||
''';
|
||||
|
||||
const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable';
|
||||
const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable';
|
||||
|
||||
/// End Cluster Summary Table Fields & Schema Queries
|
||||
|
||||
@@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback (
|
||||
PRIMARY KEY($personIdColumn, $clusterIDColumn)
|
||||
);
|
||||
''';
|
||||
const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback';
|
||||
const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
|
||||
// End Clusters Table Fields & Schema Queries
|
||||
|
||||
@@ -1,42 +1,34 @@
|
||||
/// Bounding box of a face.
|
||||
///
|
||||
/// [xMin] and [yMin] are the coordinates of the top left corner of the box, and
|
||||
/// [ x] and [y] are the minimum coordinates, so the top left corner of the box.
|
||||
/// [width] and [height] are the width and height of the box.
|
||||
///
|
||||
/// WARNING: All values are relative to the original image size, so in the range [0, 1].
|
||||
class FaceBox {
|
||||
final double xMin;
|
||||
final double yMin;
|
||||
final double x;
|
||||
final double y;
|
||||
final double width;
|
||||
final double height;
|
||||
|
||||
FaceBox({
|
||||
required this.xMin,
|
||||
required this.yMin,
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
factory FaceBox.fromJson(Map<String, dynamic> json) {
|
||||
return FaceBox(
|
||||
xMin: (json['xMin'] is int
|
||||
? (json['xMin'] as int).toDouble()
|
||||
: json['xMin'] as double),
|
||||
yMin: (json['yMin'] is int
|
||||
? (json['yMin'] as int).toDouble()
|
||||
: json['yMin'] as double),
|
||||
width: (json['width'] is int
|
||||
? (json['width'] as int).toDouble()
|
||||
: json['width'] as double),
|
||||
height: (json['height'] is int
|
||||
? (json['height'] as int).toDouble()
|
||||
: json['height'] as double),
|
||||
x: (json['x'] as double?) ?? (json['xMin'] as double),
|
||||
y: (json['y'] as double?) ?? (json['yMin'] as double),
|
||||
width: json['width'] as double,
|
||||
height: json['height'] as double,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'xMin': xMin,
|
||||
'yMin': yMin,
|
||||
'x': x,
|
||||
'y': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import "package:photos/services/machine_learning/face_ml/face_detection/detectio
|
||||
|
||||
/// Stores the face detection data, notably the bounding box and landmarks.
|
||||
///
|
||||
/// - Bounding box: [FaceBox] with xMin, yMin (so top left corner), width, height
|
||||
/// - Bounding box: [FaceBox] with x, y (minimum, so top left corner), width, height
|
||||
/// - Landmarks: list of [Landmark]s, namely leftEye, rightEye, nose, leftMouth, rightMouth
|
||||
///
|
||||
/// WARNING: All coordinates are relative to the image size, so in the range [0, 1]!
|
||||
@@ -24,8 +24,8 @@ class Detection {
|
||||
// empty box
|
||||
Detection.empty()
|
||||
: box = FaceBox(
|
||||
xMin: 0,
|
||||
yMin: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
),
|
||||
|
||||
4
mobile/lib/generated/intl/messages_cs.dart
generated
@@ -50,10 +50,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Enter person name"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
||||
"longPressAnEmailToVerifyEndToEndEncryption":
|
||||
|
||||
6
mobile/lib/generated/intl/messages_de.dart
generated
@@ -706,8 +706,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Daten exportieren"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("Gesichter"),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
|
||||
"Der Code konnte nicht aktiviert werden"),
|
||||
@@ -819,6 +817,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Falscher Wiederherstellungs-Schlüssel"),
|
||||
"indexedItems":
|
||||
MessageLookupByLibrary.simpleMessage("Indizierte Elemente"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"),
|
||||
"installManually":
|
||||
@@ -926,8 +926,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"machineLearning":
|
||||
MessageLookupByLibrary.simpleMessage("Maschinelles Lernen"),
|
||||
"magicSearch": MessageLookupByLibrary.simpleMessage("Magische Suche"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch führt, bis alle Elemente indiziert sind."),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("Verwalten"),
|
||||
"manageDeviceStorage":
|
||||
MessageLookupByLibrary.simpleMessage("Gerätespeicher verwalten"),
|
||||
|
||||
8
mobile/lib/generated/intl/messages_en.dart
generated
@@ -704,8 +704,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Export your data"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("Faces"),
|
||||
"failedToApplyCode":
|
||||
MessageLookupByLibrary.simpleMessage("Failed to apply code"),
|
||||
@@ -813,6 +811,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"incorrectRecoveryKeyTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Incorrect recovery key"),
|
||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused. It will automatically resume when device is ready."),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Insecure device"),
|
||||
"installManually":
|
||||
@@ -919,8 +919,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"machineLearning":
|
||||
MessageLookupByLibrary.simpleMessage("Machine learning"),
|
||||
"magicSearch": MessageLookupByLibrary.simpleMessage("Magic search"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("Manage"),
|
||||
"manageDeviceStorage":
|
||||
MessageLookupByLibrary.simpleMessage("Manage device storage"),
|
||||
@@ -937,6 +935,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
|
||||
"memoryCount": m33,
|
||||
"merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"),
|
||||
"mlIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"mobileWebDesktop":
|
||||
MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"),
|
||||
"moderateStrength": MessageLookupByLibrary.simpleMessage("Moderate"),
|
||||
|
||||
4
mobile/lib/generated/intl/messages_es.dart
generated
@@ -615,8 +615,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Exportar tus datos"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"failedToApplyCode":
|
||||
MessageLookupByLibrary.simpleMessage("Error al aplicar el código"),
|
||||
"failedToCancel":
|
||||
@@ -699,6 +697,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"La clave de recuperación introducida es incorrecta"),
|
||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||
"Clave de recuperación incorrecta"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"),
|
||||
"installManually":
|
||||
|
||||
4
mobile/lib/generated/intl/messages_fr.dart
generated
@@ -694,8 +694,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Exportez vos données"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("Visages"),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
|
||||
"Impossible d\'appliquer le code"),
|
||||
@@ -804,6 +802,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"La clé de secours que vous avez entrée est incorrecte"),
|
||||
"incorrectRecoveryKeyTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Clé de secours non valide"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"),
|
||||
"installManually":
|
||||
|
||||
4
mobile/lib/generated/intl/messages_it.dart
generated
@@ -671,8 +671,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage(
|
||||
"Impossibile applicare il codice"),
|
||||
"failedToCancel":
|
||||
@@ -773,6 +771,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Il codice che hai inserito non è corretto"),
|
||||
"incorrectRecoveryKeyTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Chiave di recupero errata"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Dispositivo non sicuro"),
|
||||
"installManually":
|
||||
|
||||
4
mobile/lib/generated/intl/messages_ko.dart
generated
@@ -50,10 +50,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Enter person name"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||
"locations": MessageLookupByLibrary.simpleMessage("Locations"),
|
||||
"longPressAnEmailToVerifyEndToEndEncryption":
|
||||
|
||||
6
mobile/lib/generated/intl/messages_nl.dart
generated
@@ -727,8 +727,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("Gezichten"),
|
||||
"failedToApplyCode":
|
||||
MessageLookupByLibrary.simpleMessage("Code toepassen mislukt"),
|
||||
@@ -840,6 +838,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Onjuiste herstelsleutel"),
|
||||
"indexedItems":
|
||||
MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Onveilig apparaat"),
|
||||
"installManually":
|
||||
@@ -950,8 +950,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Machine Learning"),
|
||||
"magicSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Magische zoekfunctie"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Houd er rekening mee dat dit zal resulteren in een hoger internet- en batterijverbruik totdat alle items zijn geïndexeerd."),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("Beheren"),
|
||||
"manageDeviceStorage":
|
||||
MessageLookupByLibrary.simpleMessage("Apparaatopslag beheren"),
|
||||
|
||||
4
mobile/lib/generated/intl/messages_no.dart
generated
@@ -67,11 +67,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Skriv inn e-postadressen din"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"),
|
||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"invalidEmailAddress":
|
||||
MessageLookupByLibrary.simpleMessage("Ugyldig e-postadresse"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||
|
||||
4
mobile/lib/generated/intl/messages_pl.dart
generated
@@ -115,8 +115,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Wprowadź swój klucz odzyskiwania"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"feedback": MessageLookupByLibrary.simpleMessage("Informacja zwrotna"),
|
||||
"fileTypes": MessageLookupByLibrary.simpleMessage("File types"),
|
||||
"forgotPassword":
|
||||
@@ -131,6 +129,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Kod jest nieprawidłowy"),
|
||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||
"Nieprawidłowy klucz odzyskiwania"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"Indexing is paused, will automatically resume when device is ready"),
|
||||
"invalidEmailAddress":
|
||||
MessageLookupByLibrary.simpleMessage("Nieprawidłowy adres e-mail"),
|
||||
"joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"),
|
||||
|
||||
37
mobile/lib/generated/intl/messages_pt.dart
generated
@@ -98,7 +98,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"${storageAmountInGB} GB cada vez que alguém se inscrever para um plano pago e aplica o seu código";
|
||||
|
||||
static String m25(freeAmount, storageUnit) =>
|
||||
"${freeAmount} ${storageUnit} grátis";
|
||||
"${freeAmount} ${storageUnit} livre";
|
||||
|
||||
static String m26(endDate) => "Teste gratuito acaba em ${endDate}";
|
||||
|
||||
@@ -225,6 +225,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"Eu entendo que se eu perder minha senha, posso perder meus dados, já que meus dados são <underline>criptografados de ponta a ponta</underline>."),
|
||||
"activeSessions":
|
||||
MessageLookupByLibrary.simpleMessage("Sessões ativas"),
|
||||
"addAName": MessageLookupByLibrary.simpleMessage("Adicione um nome"),
|
||||
"addANewEmail":
|
||||
MessageLookupByLibrary.simpleMessage("Adicionar um novo email"),
|
||||
"addCollaborator":
|
||||
@@ -446,7 +447,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"clubByFileName": MessageLookupByLibrary.simpleMessage(
|
||||
"Agrupar pelo nome de arquivo"),
|
||||
"clusteringProgress":
|
||||
MessageLookupByLibrary.simpleMessage("Clustering progress"),
|
||||
MessageLookupByLibrary.simpleMessage("Progresso de agrupamento"),
|
||||
"codeAppliedPageTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Código aplicado"),
|
||||
"codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -628,7 +629,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"doYouWantToDiscardTheEditsYouHaveMade":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Você quer descartar as edições que você fez?"),
|
||||
"done": MessageLookupByLibrary.simpleMessage("Concluído"),
|
||||
"done": MessageLookupByLibrary.simpleMessage("Pronto"),
|
||||
"doubleYourStorage":
|
||||
MessageLookupByLibrary.simpleMessage("Dobre seu armazenamento"),
|
||||
"download": MessageLookupByLibrary.simpleMessage("Baixar"),
|
||||
@@ -692,6 +693,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"),
|
||||
"enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage(
|
||||
"Insira a senha para criptografar seus dados"),
|
||||
"enterPersonName":
|
||||
MessageLookupByLibrary.simpleMessage("Inserir nome da pessoa"),
|
||||
"enterReferralCode": MessageLookupByLibrary.simpleMessage(
|
||||
"Insira o código de referência"),
|
||||
"enterThe6digitCodeFromnyourAuthenticatorApp":
|
||||
@@ -717,9 +720,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"exportYourData":
|
||||
MessageLookupByLibrary.simpleMessage("Exportar seus dados"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
MessageLookupByLibrary.simpleMessage("Reconhecimento facial"),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("Rostos"),
|
||||
"failedToApplyCode":
|
||||
MessageLookupByLibrary.simpleMessage("Falha ao aplicar o código"),
|
||||
@@ -761,12 +762,15 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Arquivos excluídos"),
|
||||
"filesSavedToGallery":
|
||||
MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"),
|
||||
"findPeopleByName": MessageLookupByLibrary.simpleMessage(
|
||||
"Encontre pessoas rapidamente por nome"),
|
||||
"flip": MessageLookupByLibrary.simpleMessage("Inverter"),
|
||||
"forYourMemories":
|
||||
MessageLookupByLibrary.simpleMessage("para suas memórias"),
|
||||
"forgotPassword":
|
||||
MessageLookupByLibrary.simpleMessage("Esqueceu sua senha"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||
"foundFaces":
|
||||
MessageLookupByLibrary.simpleMessage("Rostos encontrados"),
|
||||
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage(
|
||||
"Armazenamento gratuito reivindicado"),
|
||||
"freeStorageOnReferralSuccess": m24,
|
||||
@@ -814,7 +818,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"A autenticação biométrica não está configurada no seu dispositivo. Por favor, ative o Touch ID ou o Face ID no seu telefone."),
|
||||
"iOSLockOut": MessageLookupByLibrary.simpleMessage(
|
||||
"A Autenticação Biométrica está desativada. Por favor, bloqueie e desbloqueie sua tela para ativá-la."),
|
||||
"iOSOkButton": MessageLookupByLibrary.simpleMessage("Aceitar"),
|
||||
"iOSOkButton": MessageLookupByLibrary.simpleMessage("Tudo bem"),
|
||||
"ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignorar"),
|
||||
"ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage(
|
||||
"Alguns arquivos neste álbum são ignorados do envio porque eles tinham sido anteriormente excluídos do Ente."),
|
||||
@@ -830,6 +834,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage(
|
||||
"Chave de recuperação incorreta"),
|
||||
"indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"),
|
||||
"indexingIsPaused": MessageLookupByLibrary.simpleMessage(
|
||||
"A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto."),
|
||||
"insecureDevice":
|
||||
MessageLookupByLibrary.simpleMessage("Dispositivo não seguro"),
|
||||
"installManually":
|
||||
@@ -942,8 +948,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"machineLearning":
|
||||
MessageLookupByLibrary.simpleMessage("Aprendizagem de máquina"),
|
||||
"magicSearch": MessageLookupByLibrary.simpleMessage("Busca mágica"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("Gerenciar"),
|
||||
"manageDeviceStorage": MessageLookupByLibrary.simpleMessage(
|
||||
"Gerenciar o armazenamento do dispositivo"),
|
||||
@@ -961,6 +965,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
|
||||
"memoryCount": m33,
|
||||
"merchandise": MessageLookupByLibrary.simpleMessage("Produtos"),
|
||||
"mlIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados."),
|
||||
"mobileWebDesktop":
|
||||
MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"),
|
||||
"moderateStrength": MessageLookupByLibrary.simpleMessage("Moderada"),
|
||||
@@ -1021,11 +1027,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"nothingToSeeHere":
|
||||
MessageLookupByLibrary.simpleMessage("Nada para ver aqui! 👀"),
|
||||
"notifications": MessageLookupByLibrary.simpleMessage("Notificações"),
|
||||
"ok": MessageLookupByLibrary.simpleMessage("Ok"),
|
||||
"ok": MessageLookupByLibrary.simpleMessage("OK"),
|
||||
"onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"),
|
||||
"onEnte": MessageLookupByLibrary.simpleMessage(
|
||||
"Em <branding>ente</branding>"),
|
||||
"oops": MessageLookupByLibrary.simpleMessage("Ops"),
|
||||
"oops": MessageLookupByLibrary.simpleMessage("Opa"),
|
||||
"oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage(
|
||||
"Ops, não foi possível salvar edições"),
|
||||
"oopsSomethingWentWrong":
|
||||
@@ -1064,6 +1070,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"),
|
||||
"pendingSync":
|
||||
MessageLookupByLibrary.simpleMessage("Sincronização pendente"),
|
||||
"people": MessageLookupByLibrary.simpleMessage("Pessoas"),
|
||||
"peopleUsingYourCode":
|
||||
MessageLookupByLibrary.simpleMessage("Pessoas que usam seu código"),
|
||||
"permDeleteWarning": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -1197,6 +1204,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"removeParticipant":
|
||||
MessageLookupByLibrary.simpleMessage("Remover participante"),
|
||||
"removeParticipantBody": m43,
|
||||
"removePersonLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"),
|
||||
"removePublicLink":
|
||||
MessageLookupByLibrary.simpleMessage("Remover link público"),
|
||||
"removeShareItemsWarning": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -1260,7 +1269,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"searchDatesEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Pesquisar por data, mês ou ano"),
|
||||
"searchFaceEmptySection": MessageLookupByLibrary.simpleMessage(
|
||||
"Encontre todas as fotos de uma pessoa"),
|
||||
"Pessoas serão exibidas aqui uma vez que a indexação é feita"),
|
||||
"searchFileTypesAndNamesEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"),
|
||||
"searchHint1": MessageLookupByLibrary.simpleMessage(
|
||||
@@ -1303,7 +1312,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"As pastas selecionadas serão criptografadas e armazenadas em backup"),
|
||||
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved":
|
||||
MessageLookupByLibrary.simpleMessage(
|
||||
"Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo."),
|
||||
"Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira."),
|
||||
"selectedPhotos": m46,
|
||||
"selectedPhotosWithYours": m47,
|
||||
"send": MessageLookupByLibrary.simpleMessage("Enviar"),
|
||||
|
||||
25
mobile/lib/generated/intl/messages_zh.dart
generated
@@ -124,7 +124,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
|
||||
static String m37(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天";
|
||||
|
||||
static String m38(endDate) => "免费试用有效期至 ${endDate}。\n您可以随后购买付费计划。";
|
||||
static String m38(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。";
|
||||
|
||||
static String m39(toEmail) => "请给我们发送电子邮件至 ${toEmail}";
|
||||
|
||||
@@ -206,6 +206,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage(
|
||||
"我明白,如果我丢失密码,我可能会丢失我的数据,因为我的数据是 <underline>端到端加密的</underline>。"),
|
||||
"activeSessions": MessageLookupByLibrary.simpleMessage("已登录的设备"),
|
||||
"addAName": MessageLookupByLibrary.simpleMessage("添加一个名称"),
|
||||
"addANewEmail": MessageLookupByLibrary.simpleMessage("添加新的电子邮件"),
|
||||
"addCollaborator": MessageLookupByLibrary.simpleMessage("添加协作者"),
|
||||
"addCollaborators": m0,
|
||||
@@ -382,8 +383,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"close": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||
"clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按拍摄时间分组"),
|
||||
"clubByFileName": MessageLookupByLibrary.simpleMessage("按文件名排序"),
|
||||
"clusteringProgress":
|
||||
MessageLookupByLibrary.simpleMessage("Clustering progress"),
|
||||
"clusteringProgress": MessageLookupByLibrary.simpleMessage("聚类进展"),
|
||||
"codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("代码已应用"),
|
||||
"codeCopiedToClipboard":
|
||||
MessageLookupByLibrary.simpleMessage("代码已复制到剪贴板"),
|
||||
@@ -576,6 +576,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"enterPassword": MessageLookupByLibrary.simpleMessage("输入密码"),
|
||||
"enterPasswordToEncrypt":
|
||||
MessageLookupByLibrary.simpleMessage("输入我们可以用来加密您的数据的密码"),
|
||||
"enterPersonName": MessageLookupByLibrary.simpleMessage("输入人物名称"),
|
||||
"enterReferralCode": MessageLookupByLibrary.simpleMessage("输入推荐代码"),
|
||||
"enterThe6digitCodeFromnyourAuthenticatorApp":
|
||||
MessageLookupByLibrary.simpleMessage("从你的身份验证器应用中\n输入6位数字代码"),
|
||||
@@ -594,10 +595,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接有效期。"),
|
||||
"exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"),
|
||||
"exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"),
|
||||
"faceRecognition":
|
||||
MessageLookupByLibrary.simpleMessage("Face recognition"),
|
||||
"faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"Please note that this will result in a higher bandwidth and battery usage until all items are indexed."),
|
||||
"faceRecognition": MessageLookupByLibrary.simpleMessage("人脸识别"),
|
||||
"faces": MessageLookupByLibrary.simpleMessage("人脸"),
|
||||
"failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法使用此代码"),
|
||||
"failedToCancel": MessageLookupByLibrary.simpleMessage("取消失败"),
|
||||
@@ -629,10 +627,11 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"),
|
||||
"filesSavedToGallery":
|
||||
MessageLookupByLibrary.simpleMessage("多个文件已保存到相册"),
|
||||
"findPeopleByName": MessageLookupByLibrary.simpleMessage("按名称快速查找人物"),
|
||||
"flip": MessageLookupByLibrary.simpleMessage("上下翻转"),
|
||||
"forYourMemories": MessageLookupByLibrary.simpleMessage("为您的回忆"),
|
||||
"forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"),
|
||||
"foundFaces": MessageLookupByLibrary.simpleMessage("已找到的人脸"),
|
||||
"freeStorageClaimed": MessageLookupByLibrary.simpleMessage("已领取的免费存储"),
|
||||
"freeStorageOnReferralSuccess": m24,
|
||||
"freeStorageSpace": m25,
|
||||
@@ -686,6 +685,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"incorrectRecoveryKeyTitle":
|
||||
MessageLookupByLibrary.simpleMessage("不正确的恢复密钥"),
|
||||
"indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"),
|
||||
"indexingIsPaused":
|
||||
MessageLookupByLibrary.simpleMessage("索引已暂停。当设备准备就绪时,它将自动恢复。"),
|
||||
"insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"),
|
||||
"installManually": MessageLookupByLibrary.simpleMessage("手动安装"),
|
||||
"invalidEmailAddress":
|
||||
@@ -777,8 +778,6 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"lostDevice": MessageLookupByLibrary.simpleMessage("设备丢失?"),
|
||||
"machineLearning": MessageLookupByLibrary.simpleMessage("机器学习"),
|
||||
"magicSearch": MessageLookupByLibrary.simpleMessage("魔法搜索"),
|
||||
"magicSearchDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"请注意,在所有项目完成索引之前,这将使用更高的带宽和电量。"),
|
||||
"manage": MessageLookupByLibrary.simpleMessage("管理"),
|
||||
"manageDeviceStorage": MessageLookupByLibrary.simpleMessage("管理设备存储"),
|
||||
"manageFamily": MessageLookupByLibrary.simpleMessage("管理家庭计划"),
|
||||
@@ -793,6 +792,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"matrix": MessageLookupByLibrary.simpleMessage("Matrix"),
|
||||
"memoryCount": m33,
|
||||
"merchandise": MessageLookupByLibrary.simpleMessage("商品"),
|
||||
"mlIndexingDescription": MessageLookupByLibrary.simpleMessage(
|
||||
"请注意,机器学习将使用更高的带宽和更多的电量,直到所有项目都被索引为止。"),
|
||||
"mobileWebDesktop":
|
||||
MessageLookupByLibrary.simpleMessage("移动端, 网页端, 桌面端"),
|
||||
"moderateStrength": MessageLookupByLibrary.simpleMessage("中等"),
|
||||
@@ -878,6 +879,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"paymentFailedTalkToProvider": m37,
|
||||
"pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"),
|
||||
"pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"),
|
||||
"people": MessageLookupByLibrary.simpleMessage("人物"),
|
||||
"peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("使用您的代码的人"),
|
||||
"permDeleteWarning":
|
||||
MessageLookupByLibrary.simpleMessage("回收站中的所有项目将被永久删除\n\n此操作无法撤消"),
|
||||
@@ -977,6 +979,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"removeLink": MessageLookupByLibrary.simpleMessage("移除链接"),
|
||||
"removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"),
|
||||
"removeParticipantBody": m43,
|
||||
"removePersonLabel": MessageLookupByLibrary.simpleMessage("移除人物标签"),
|
||||
"removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"),
|
||||
"removeShareItemsWarning":
|
||||
MessageLookupByLibrary.simpleMessage("您要删除的某些项目是由其他人添加的,您将无法访问它们"),
|
||||
@@ -1023,7 +1026,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"searchDatesEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("按日期搜索,月份或年份"),
|
||||
"searchFaceEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("查找一个人的所有照片"),
|
||||
MessageLookupByLibrary.simpleMessage("待索引完成后,人物将显示在此处"),
|
||||
"searchFileTypesAndNamesEmptySection":
|
||||
MessageLookupByLibrary.simpleMessage("文件类型和名称"),
|
||||
"searchHint1": MessageLookupByLibrary.simpleMessage("在设备上快速搜索"),
|
||||
|
||||
28
mobile/lib/generated/l10n.dart
generated
@@ -2876,11 +2876,11 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please note that this will result in a higher bandwidth and battery usage until all items are indexed.`
|
||||
String get magicSearchDescription {
|
||||
/// `Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.`
|
||||
String get mlIndexingDescription {
|
||||
return Intl.message(
|
||||
'Please note that this will result in a higher bandwidth and battery usage until all items are indexed.',
|
||||
name: 'magicSearchDescription',
|
||||
'Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.',
|
||||
name: 'mlIndexingDescription',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
@@ -8764,16 +8764,6 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Please note that this will result in a higher bandwidth and battery usage until all items are indexed.`
|
||||
String get faceRecognitionIndexingDescription {
|
||||
return Intl.message(
|
||||
'Please note that this will result in a higher bandwidth and battery usage until all items are indexed.',
|
||||
name: 'faceRecognitionIndexingDescription',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Found faces`
|
||||
String get foundFaces {
|
||||
return Intl.message(
|
||||
@@ -8793,6 +8783,16 @@ class S {
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Indexing is paused. It will automatically resume when device is ready.`
|
||||
String get indexingIsPaused {
|
||||
return Intl.message(
|
||||
'Indexing is paused. It will automatically resume when device is ready.',
|
||||
name: 'indexingIsPaused',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -1212,5 +1212,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -409,7 +409,7 @@
|
||||
"manageDeviceStorage": "Manage device storage",
|
||||
"machineLearning": "Machine learning",
|
||||
"magicSearch": "Magic search",
|
||||
"magicSearchDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"mlIndexingDescription": "Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"loadingModel": "Downloading models...",
|
||||
"waitingForWifi": "Waiting for WiFi...",
|
||||
"status": "Status",
|
||||
@@ -1233,7 +1233,7 @@
|
||||
"autoPair": "Auto pair",
|
||||
"pairWithPin": "Pair with PIN",
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
}
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready."
|
||||
}
|
||||
|
||||
@@ -986,5 +986,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -1167,5 +1167,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -1129,5 +1129,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -24,5 +24,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -1230,5 +1230,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -38,5 +38,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -125,5 +125,6 @@
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"clusteringProgress": "Clustering progress",
|
||||
"indexingIsPaused": "Indexing is paused, will automatically resume when device is ready"
|
||||
}
|
||||
@@ -24,13 +24,13 @@
|
||||
"deleteRequestSLAText": "Sua solicitação será processada em até 72 horas.",
|
||||
"deleteEmailRequest": "Por favor, envie um e-mail para <warning>account-deletion@ente.io</warning> a partir do seu endereço de e-mail registrado.",
|
||||
"entePhotosPerm": "Ente <i>precisa de permissão para</i> preservar suas fotos",
|
||||
"ok": "Ok",
|
||||
"ok": "OK",
|
||||
"createAccount": "Criar uma conta",
|
||||
"createNewAccount": "Criar nova conta",
|
||||
"password": "Senha",
|
||||
"confirmPassword": "Confirme sua senha",
|
||||
"activeSessions": "Sessões ativas",
|
||||
"oops": "Ops",
|
||||
"oops": "Opa",
|
||||
"somethingWentWrongPleaseTryAgain": "Algo deu errado. Por favor, tente outra vez",
|
||||
"thisWillLogYouOutOfThisDevice": "Isso fará com que você saia deste dispositivo!",
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Isso fará com que você saia do seguinte dispositivo:",
|
||||
@@ -265,7 +265,7 @@
|
||||
"somethingWentWrong": "Algo deu errado",
|
||||
"sendInvite": "Enviar convite",
|
||||
"shareTextRecommendUsingEnte": "Baixe o Ente para que possamos compartilhar facilmente fotos e vídeos de qualidade original\n\nhttps://ente.io",
|
||||
"done": "Concluído",
|
||||
"done": "Pronto",
|
||||
"applyCodeTitle": "Aplicar código",
|
||||
"enterCodeDescription": "Digite o código fornecido pelo seu amigo para reivindicar o armazenamento gratuito para vocês dois",
|
||||
"apply": "Aplicar",
|
||||
@@ -409,7 +409,7 @@
|
||||
"manageDeviceStorage": "Gerenciar o armazenamento do dispositivo",
|
||||
"machineLearning": "Aprendizagem de máquina",
|
||||
"magicSearch": "Busca mágica",
|
||||
"magicSearchDescription": "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados.",
|
||||
"mlIndexingDescription": "Por favor, note que isso resultará em uma largura de banda maior e uso de bateria até que todos os itens sejam indexados.",
|
||||
"loadingModel": "Fazendo download de modelos...",
|
||||
"waitingForWifi": "Esperando por Wi-Fi...",
|
||||
"status": "Estado",
|
||||
@@ -948,7 +948,7 @@
|
||||
"someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos arquivos que você está tentando excluir só estão disponíveis no seu dispositivo e não podem ser recuperados se forem excluídos",
|
||||
"theyWillBeDeletedFromAllAlbums": "Ele será excluído de todos os álbuns.",
|
||||
"someItemsAreInBothEnteAndYourDevice": "Alguns itens estão tanto no Ente quanto no seu dispositivo.",
|
||||
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo.",
|
||||
"selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira.",
|
||||
"theseItemsWillBeDeletedFromYourDevice": "Estes itens serão excluídos do seu dispositivo.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo deu errado. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contato com nossa equipe de suporte.",
|
||||
"error": "Erro",
|
||||
@@ -987,7 +987,7 @@
|
||||
"fileTypesAndNames": "Tipos de arquivo e nomes",
|
||||
"location": "Local",
|
||||
"moments": "Momentos",
|
||||
"searchFaceEmptySection": "Encontre todas as fotos de uma pessoa",
|
||||
"searchFaceEmptySection": "Pessoas serão exibidas aqui uma vez que a indexação é feita",
|
||||
"searchDatesEmptySection": "Pesquisar por data, mês ou ano",
|
||||
"searchLocationEmptySection": "Fotos de grupo que estão sendo tiradas em algum raio da foto",
|
||||
"searchPeopleEmptySection": "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui",
|
||||
@@ -1042,7 +1042,7 @@
|
||||
"@storageUsageInfo": {
|
||||
"description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used"
|
||||
},
|
||||
"freeStorageSpace": "{freeAmount} {storageUnit} grátis",
|
||||
"freeStorageSpace": "{freeAmount} {storageUnit} livre",
|
||||
"appVersion": "Versão: {versionValue}",
|
||||
"verifyIDLabel": "Verificar",
|
||||
"fileInfoAddDescHint": "Adicionar descrição...",
|
||||
@@ -1102,7 +1102,7 @@
|
||||
"@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."
|
||||
},
|
||||
"iOSOkButton": "Aceitar",
|
||||
"iOSOkButton": "Tudo bem",
|
||||
"@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."
|
||||
},
|
||||
@@ -1171,6 +1171,7 @@
|
||||
}
|
||||
},
|
||||
"faces": "Rostos",
|
||||
"people": "Pessoas",
|
||||
"contents": "Conteúdos",
|
||||
"addNew": "Adicionar novo",
|
||||
"@addNew": {
|
||||
@@ -1196,14 +1197,14 @@
|
||||
"verifyPasskey": "Verificar chave de acesso",
|
||||
"playOnTv": "Reproduzir álbum na TV",
|
||||
"pair": "Parear",
|
||||
"autoPair": "Pareamento automático",
|
||||
"pairWithPin": "Parear com PIN",
|
||||
"deviceNotFound": "Dispositivo não encontrado",
|
||||
"castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.",
|
||||
"deviceCodeHint": "Insira o código",
|
||||
"joinDiscord": "Junte-se ao Discord",
|
||||
"locations": "Locais",
|
||||
"descriptions": "Descrições",
|
||||
"addAName": "Adicione um nome",
|
||||
"findPeopleByName": "Encontre pessoas rapidamente por nome",
|
||||
"addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}",
|
||||
"addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.",
|
||||
@@ -1216,6 +1217,8 @@
|
||||
"customEndpoint": "Conectado a {endpoint}",
|
||||
"createCollaborativeLink": "Criar link colaborativo",
|
||||
"search": "Pesquisar",
|
||||
"enterPersonName": "Inserir nome da pessoa",
|
||||
"removePersonLabel": "Remover etiqueta da pessoa",
|
||||
"autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.",
|
||||
"manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.",
|
||||
"connectToDevice": "Conectar ao dispositivo",
|
||||
@@ -1227,8 +1230,10 @@
|
||||
"castIPMismatchTitle": "Falha ao transmitir álbum",
|
||||
"castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.",
|
||||
"pairingComplete": "Pareamento concluído",
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"autoPair": "Pareamento automático",
|
||||
"pairWithPin": "Parear com PIN",
|
||||
"faceRecognition": "Reconhecimento facial",
|
||||
"foundFaces": "Rostos encontrados",
|
||||
"clusteringProgress": "Progresso de agrupamento",
|
||||
"indexingIsPaused": "A indexação está pausada, será retomada automaticamente quando o dispositivo estiver pronto."
|
||||
}
|
||||
@@ -409,7 +409,7 @@
|
||||
"manageDeviceStorage": "管理设备存储",
|
||||
"machineLearning": "机器学习",
|
||||
"magicSearch": "魔法搜索",
|
||||
"magicSearchDescription": "请注意,在所有项目完成索引之前,这将使用更高的带宽和电量。",
|
||||
"mlIndexingDescription": "请注意,机器学习将使用更高的带宽和更多的电量,直到所有项目都被索引为止。",
|
||||
"loadingModel": "正在下载模型...",
|
||||
"waitingForWifi": "正在等待 WiFi...",
|
||||
"status": "状态",
|
||||
@@ -569,7 +569,7 @@
|
||||
"freeTrialValidTill": "免费试用有效期至 {endDate}",
|
||||
"validTill": "有效期至 {endDate}",
|
||||
"addOnValidTill": "您的 {storageAmount} 插件有效期至 {endDate}",
|
||||
"playStoreFreeTrialValidTill": "免费试用有效期至 {endDate}。\n您可以随后购买付费计划。",
|
||||
"playStoreFreeTrialValidTill": "免费试用有效期至 {endDate}。\n在此之后您可以选择付费计划。",
|
||||
"subWillBeCancelledOn": "您的订阅将于 {endDate} 取消",
|
||||
"subscription": "订阅",
|
||||
"paymentDetails": "付款明细",
|
||||
@@ -987,7 +987,7 @@
|
||||
"fileTypesAndNames": "文件类型和名称",
|
||||
"location": "地理位置",
|
||||
"moments": "瞬间",
|
||||
"searchFaceEmptySection": "查找一个人的所有照片",
|
||||
"searchFaceEmptySection": "待索引完成后,人物将显示在此处",
|
||||
"searchDatesEmptySection": "按日期搜索,月份或年份",
|
||||
"searchLocationEmptySection": "在照片的一定半径内拍摄的几组照片",
|
||||
"searchPeopleEmptySection": "邀请他人,您将在此看到他们分享的所有照片",
|
||||
@@ -1171,6 +1171,7 @@
|
||||
}
|
||||
},
|
||||
"faces": "人脸",
|
||||
"people": "人物",
|
||||
"contents": "内容",
|
||||
"addNew": "新建",
|
||||
"@addNew": {
|
||||
@@ -1196,14 +1197,14 @@
|
||||
"verifyPasskey": "验证通行密钥",
|
||||
"playOnTv": "在电视上播放相册",
|
||||
"pair": "配对",
|
||||
"autoPair": "自动配对",
|
||||
"pairWithPin": "用 PIN 配对",
|
||||
"deviceNotFound": "未发现设备",
|
||||
"castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
|
||||
"deviceCodeHint": "输入代码",
|
||||
"joinDiscord": "加入 Discord",
|
||||
"locations": "位置",
|
||||
"descriptions": "描述",
|
||||
"addAName": "添加一个名称",
|
||||
"findPeopleByName": "按名称快速查找人物",
|
||||
"addViewers": "{count, plural, zero {添加查看者} one {添加查看者} other {添加查看者}}",
|
||||
"addCollaborators": "{count, plural, zero {添加协作者} one {添加协作者} other {添加协作者}}",
|
||||
"longPressAnEmailToVerifyEndToEndEncryption": "长按电子邮件以验证端到端加密。",
|
||||
@@ -1216,6 +1217,8 @@
|
||||
"customEndpoint": "已连接至 {endpoint}",
|
||||
"createCollaborativeLink": "创建协作链接",
|
||||
"search": "搜索",
|
||||
"enterPersonName": "输入人物名称",
|
||||
"removePersonLabel": "移除人物标签",
|
||||
"autoPairDesc": "自动配对仅适用于支持 Chromecast 的设备。",
|
||||
"manualPairDesc": "用 PIN 码配对适用于您希望在其上查看相册的任何屏幕。",
|
||||
"connectToDevice": "连接到设备",
|
||||
@@ -1227,8 +1230,10 @@
|
||||
"castIPMismatchTitle": "投放相册失败",
|
||||
"castIPMismatchBody": "请确保您的设备与电视处于同一网络。",
|
||||
"pairingComplete": "配对完成",
|
||||
"faceRecognition": "Face recognition",
|
||||
"faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.",
|
||||
"foundFaces": "Found faces",
|
||||
"clusteringProgress": "Clustering progress"
|
||||
"autoPair": "自动配对",
|
||||
"pairWithPin": "用 PIN 配对",
|
||||
"faceRecognition": "人脸识别",
|
||||
"foundFaces": "已找到的人脸",
|
||||
"clusteringProgress": "聚类进展",
|
||||
"indexingIsPaused": "索引已暂停。当设备准备就绪时,它将自动恢复。"
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/ui/tools/app_lock.dart';
|
||||
import 'package:photos/ui/tools/lock_screen.dart';
|
||||
import 'package:photos/utils/crypto_util.dart';
|
||||
import "package:photos/utils/email_util.dart";
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
import 'package:photos/utils/local_settings.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -180,6 +181,16 @@ void _headlessTaskHandler(HeadlessTask task) {
|
||||
}
|
||||
|
||||
Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
bool initComplete = false;
|
||||
Future.delayed(const Duration(seconds: 15), () {
|
||||
if (!initComplete && !isBackground) {
|
||||
sendLogsForInit(
|
||||
"support@ente.io",
|
||||
"Stuck on splash screen for >= 15 seconds",
|
||||
null,
|
||||
);
|
||||
}
|
||||
});
|
||||
_isProcessRunning = true;
|
||||
_logger.info("Initializing... inBG =$isBackground via: $via");
|
||||
final SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||
@@ -235,19 +246,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
|
||||
unawaited(SemanticSearchService.instance.init());
|
||||
MachineLearningController.instance.init();
|
||||
// Can not including existing tf/ml binaries as they are not being built
|
||||
// from source.
|
||||
// See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819
|
||||
if (!UpdateService.instance.isFdroidFlavor()) {
|
||||
// unawaited(ObjectDetectionService.instance.init());
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
FaceMlService.instance.listenIndexOnDiffSync();
|
||||
FaceMlService.instance.listenOnPeopleChangedSync();
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
if (flagService.faceSearchEnabled) {
|
||||
unawaited(FaceMlService.instance.init());
|
||||
} else {
|
||||
if (LocalSettings.instance.isFaceIndexingEnabled) {
|
||||
unawaited(LocalSettings.instance.toggleFaceIndexing());
|
||||
}
|
||||
}
|
||||
PersonService.init(
|
||||
@@ -256,6 +259,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
preferences,
|
||||
);
|
||||
|
||||
initComplete = true;
|
||||
_logger.info("Initialization done");
|
||||
}
|
||||
|
||||
|
||||
@@ -151,9 +151,7 @@ class FavoritesService {
|
||||
final collectionID = await _getOrCreateFavoriteCollectionID();
|
||||
final List<EnteFile> files = [file];
|
||||
if (file.uploadedFileID == null) {
|
||||
file.collectionID = collectionID;
|
||||
await _filesDB.insert(file);
|
||||
Bus.instance.fire(CollectionUpdatedEvent(collectionID, files, "addTFav"));
|
||||
throw AssertionError("Can only favorite uploaded items");
|
||||
} else {
|
||||
await _collectionsService.addOrCopyToCollection(collectionID, files);
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ class LocalFileUpdateService {
|
||||
} else if (e.reason == InvalidReason.imageToLivePhotoTypeChanged) {
|
||||
fileType = FileType.livePhoto;
|
||||
}
|
||||
final int count = await FilesDB.instance.markFilesForReUpload(
|
||||
await FilesDB.instance.markFilesForReUpload(
|
||||
userID,
|
||||
file.localID!,
|
||||
file.title,
|
||||
@@ -202,8 +202,7 @@ class LocalFileUpdateService {
|
||||
file.modificationTime!,
|
||||
fileType,
|
||||
);
|
||||
_logger.fine('fileType changed for ${file.tag} to ${e.reason} for '
|
||||
'$count files');
|
||||
_logger.fine('fileType changed for ${file.tag} to ${e.reason} for ');
|
||||
} else {
|
||||
_logger.severe("failed to check hash: invalid file ${file.tag}", e);
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import "package:photos/services/ignored_files_service.dart";
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import "package:photos/utils/sqlite_util.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class LocalSyncService {
|
||||
@@ -184,7 +184,7 @@ class LocalSyncService {
|
||||
if (hasUnsyncedFiles) {
|
||||
await _db.insertMultiple(
|
||||
localDiffResult.uniqueLocalFiles!,
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore,
|
||||
);
|
||||
_logger.info(
|
||||
"Inserted ${localDiffResult.uniqueLocalFiles?.length} "
|
||||
@@ -321,7 +321,7 @@ class LocalSyncService {
|
||||
files.removeWhere((file) => existingLocalDs.contains(file.localID));
|
||||
await _db.insertMultiple(
|
||||
files,
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore,
|
||||
conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore,
|
||||
);
|
||||
_logger.info('Inserted ${files.length} files');
|
||||
Bus.instance.fire(
|
||||
|
||||
@@ -5,17 +5,15 @@ import "package:ml_linalg/linalg.dart";
|
||||
/// Calculates the cosine distance between two embeddings/vectors using SIMD from ml_linalg
|
||||
///
|
||||
/// WARNING: This assumes both vectors are already normalized!
|
||||
/// WARNING: For even more performance, consider calculating the logic below inline!
|
||||
@pragma("vm:prefer-inline")
|
||||
double cosineDistanceSIMD(Vector vector1, Vector vector2) {
|
||||
if (vector1.length != vector2.length) {
|
||||
throw ArgumentError('Vectors must be the same length');
|
||||
}
|
||||
|
||||
return 1 - vector1.dot(vector2);
|
||||
}
|
||||
|
||||
/// Calculates the cosine distance between two embeddings/vectors using SIMD from ml_linalg
|
||||
///
|
||||
/// WARNING: Only use when you're not sure if vectors are normalized. If you're sure they are, use [cosineDistanceSIMD] instead for better performance.
|
||||
/// WARNING: Only use when you're not sure if vectors are normalized. If you're sure they are, use [cosineDistanceSIMD] instead for better performance, or inline for best performance.
|
||||
double cosineDistanceSIMDSafe(Vector vector1, Vector vector2) {
|
||||
if (vector1.length != vector2.length) {
|
||||
throw ArgumentError('Vectors must be the same length');
|
||||
|
||||
@@ -8,6 +8,18 @@ class GeneralFaceMlException implements Exception {
|
||||
String toString() => 'GeneralFaceMlException: $message';
|
||||
}
|
||||
|
||||
class ThumbnailRetrievalException implements Exception {
|
||||
final String message;
|
||||
final StackTrace stackTrace;
|
||||
|
||||
ThumbnailRetrievalException(this.message, this.stackTrace);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ThumbnailRetrievalException: $message\n$stackTrace';
|
||||
}
|
||||
}
|
||||
|
||||
class CouldNotRetrieveAnyFileData implements Exception {}
|
||||
|
||||
class CouldNotInitializeFaceDetector implements Exception {}
|
||||
|
||||
@@ -310,5 +310,9 @@ class FaceResultBuilder {
|
||||
}
|
||||
|
||||
int getFileIdFromFaceId(String faceId) {
|
||||
return int.parse(faceId.split("_")[0]);
|
||||
return int.parse(faceId.split("_").first);
|
||||
}
|
||||
|
||||
int? tryGetFileIdFromFaceId(String faceId) {
|
||||
return int.tryParse(faceId.split("_").first);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import "package:photos/face/db.dart";
|
||||
import "package:photos/face/model/person.dart";
|
||||
import "package:photos/generated/protos/ente/common/vector.pb.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_clustering/cosine_distance.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart";
|
||||
import "package:photos/services/machine_learning/face_ml/face_ml_result.dart";
|
||||
@@ -118,7 +117,11 @@ class ClusterFeedbackService {
|
||||
|
||||
final sortingStartTime = DateTime.now();
|
||||
if (extremeFilesFirst) {
|
||||
await _sortSuggestionsOnDistanceToPerson(person, finalSuggestions);
|
||||
try {
|
||||
await _sortSuggestionsOnDistanceToPerson(person, finalSuggestions);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error in sorting suggestions", e, s);
|
||||
}
|
||||
}
|
||||
_logger.info(
|
||||
'getSuggestionForPerson post-processing suggestions took ${DateTime.now().difference(findSuggestionsTime).inMilliseconds} ms, of which sorting took ${DateTime.now().difference(sortingStartTime).inMilliseconds} ms and getting files took ${getFilesTime.difference(findSuggestionsTime).inMilliseconds} ms',
|
||||
@@ -157,7 +160,8 @@ class ClusterFeedbackService {
|
||||
fileIDToCreationTime: fileIDToCreationTime,
|
||||
distanceThreshold: 0.20,
|
||||
);
|
||||
if (clusterResult == null || clusterResult.isEmpty) {
|
||||
if (clusterResult.isEmpty) {
|
||||
_logger.warning('No clusters found or something went wrong');
|
||||
return;
|
||||
}
|
||||
final newFaceIdToClusterID = clusterResult.newFaceIdToCluster;
|
||||
@@ -165,7 +169,7 @@ class ClusterFeedbackService {
|
||||
// Update the deleted faces
|
||||
await FaceMLDataDB.instance.forceUpdateClusterIds(newFaceIdToClusterID);
|
||||
await FaceMLDataDB.instance
|
||||
.clusterSummaryUpdate(clusterResult.newClusterSummaries!);
|
||||
.clusterSummaryUpdate(clusterResult.newClusterSummaries);
|
||||
|
||||
// Make sure the deleted faces don't get suggested in the future
|
||||
final notClusterIdToPersonId = <int, String>{};
|
||||
@@ -209,7 +213,7 @@ class ClusterFeedbackService {
|
||||
fileIDToCreationTime: fileIDToCreationTime,
|
||||
distanceThreshold: 0.20,
|
||||
);
|
||||
if (clusterResult == null || clusterResult.isEmpty) {
|
||||
if (clusterResult.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final newFaceIdToClusterID = clusterResult.newFaceIdToCluster;
|
||||
@@ -217,7 +221,7 @@ class ClusterFeedbackService {
|
||||
// Update the deleted faces
|
||||
await FaceMLDataDB.instance.forceUpdateClusterIds(newFaceIdToClusterID);
|
||||
await FaceMLDataDB.instance
|
||||
.clusterSummaryUpdate(clusterResult.newClusterSummaries!);
|
||||
.clusterSummaryUpdate(clusterResult.newClusterSummaries);
|
||||
|
||||
Bus.instance.fire(
|
||||
PeopleChangedEvent(
|
||||
@@ -345,9 +349,7 @@ class ClusterFeedbackService {
|
||||
distanceThreshold: 0.22,
|
||||
);
|
||||
|
||||
if (clusterResult == null ||
|
||||
clusterResult.newClusterIdToFaceIds == null ||
|
||||
clusterResult.isEmpty) {
|
||||
if (clusterResult.isEmpty) {
|
||||
_logger.warning(
|
||||
'[CheckMixedClusters] Clustering did not seem to work for cluster $clusterID of size ${allClusterToFaceCount[clusterID]}',
|
||||
);
|
||||
@@ -355,7 +357,7 @@ class ClusterFeedbackService {
|
||||
}
|
||||
|
||||
final newClusterIdToCount =
|
||||
clusterResult.newClusterIdToFaceIds!.map((key, value) {
|
||||
clusterResult.newClusterIdToFaceIds.map((key, value) {
|
||||
return MapEntry(key, value.length);
|
||||
});
|
||||
final amountOfNewClusters = newClusterIdToCount.length;
|
||||
@@ -424,6 +426,11 @@ class ClusterFeedbackService {
|
||||
|
||||
final embeddings = await faceMlDb.getFaceEmbeddingMapForFaces(faceIDs);
|
||||
|
||||
if (embeddings.isEmpty) {
|
||||
_logger.warning('No embeddings found for cluster $clusterID');
|
||||
return ClusteringResult.empty();
|
||||
}
|
||||
|
||||
final fileIDToCreationTime =
|
||||
await FilesDB.instance.getFileIDToCreationTime();
|
||||
|
||||
@@ -434,13 +441,13 @@ class ClusterFeedbackService {
|
||||
distanceThreshold: 0.22,
|
||||
);
|
||||
|
||||
if (clusterResult == null || clusterResult.newClusterIdToFaceIds == null || clusterResult.isEmpty) {
|
||||
if (clusterResult.isEmpty) {
|
||||
_logger.warning('No clusters found or something went wrong');
|
||||
return ClusteringResult(newFaceIdToCluster: {});
|
||||
return ClusteringResult.empty();
|
||||
}
|
||||
|
||||
final clusterIdToCount =
|
||||
clusterResult.newClusterIdToFaceIds!.map((key, value) {
|
||||
clusterResult.newClusterIdToFaceIds.map((key, value) {
|
||||
return MapEntry(key, value.length);
|
||||
});
|
||||
final amountOfNewClusters = clusterIdToCount.length;
|
||||
@@ -452,7 +459,7 @@ class ClusterFeedbackService {
|
||||
if (kDebugMode) {
|
||||
final Set allClusteredFaceIDsSet = {};
|
||||
for (final List<String> value
|
||||
in clusterResult.newClusterIdToFaceIds!.values) {
|
||||
in clusterResult.newClusterIdToFaceIds.values) {
|
||||
allClusteredFaceIDsSet.addAll(value);
|
||||
}
|
||||
assert((originalFaceIDsSet.difference(allClusteredFaceIDsSet)).isEmpty);
|
||||
@@ -537,8 +544,7 @@ class ClusterFeedbackService {
|
||||
EVector.fromBuffer(clusterSummary.$1).values,
|
||||
dtype: DType.float32,
|
||||
);
|
||||
final bigClustersMeanDistance =
|
||||
cosineDistanceSIMD(biggestMean, currentMean);
|
||||
final bigClustersMeanDistance = 1 - biggestMean.dot(currentMean);
|
||||
_logger.info(
|
||||
"Mean distance between biggest cluster and current cluster: $bigClustersMeanDistance",
|
||||
);
|
||||
@@ -595,8 +601,7 @@ class ClusterFeedbackService {
|
||||
final List<double> trueDistances = [];
|
||||
for (final biggestEmbedding in biggestSampledEmbeddings) {
|
||||
for (final currentEmbedding in currentSampledEmbeddings) {
|
||||
distances
|
||||
.add(cosineDistanceSIMD(biggestEmbedding, currentEmbedding));
|
||||
distances.add(1 - biggestEmbedding.dot(currentEmbedding));
|
||||
trueDistances.add(
|
||||
biggestEmbedding.distanceTo(
|
||||
currentEmbedding,
|
||||
@@ -686,7 +691,7 @@ class ClusterFeedbackService {
|
||||
clusterAvgBigClusters,
|
||||
personClusters,
|
||||
ignoredClusters,
|
||||
(minimumSize == 100) ? goodMeanDistance + 0.15 : goodMeanDistance,
|
||||
goodMeanDistance,
|
||||
);
|
||||
w?.log(
|
||||
'Calculate suggestions using mean for ${clusterAvgBigClusters.length} clusters of min size $minimumSize',
|
||||
@@ -789,7 +794,7 @@ class ClusterFeedbackService {
|
||||
final List<double> distances = [];
|
||||
for (final otherEmbedding in sampledOtherEmbeddings) {
|
||||
for (final embedding in sampledEmbeddings) {
|
||||
distances.add(cosineDistanceSIMD(embedding, otherEmbedding));
|
||||
distances.add(1 - embedding.dot(otherEmbedding));
|
||||
}
|
||||
}
|
||||
distances.sort();
|
||||
@@ -1041,11 +1046,25 @@ class ClusterFeedbackService {
|
||||
await faceMlDb.getClusterToClusterSummary(personClusters);
|
||||
final clusterSummaryCallTime = DateTime.now();
|
||||
|
||||
// remove personClusters that don't have any summary
|
||||
for (final clusterID in personClusters.toSet()) {
|
||||
if (!personClusterToSummary.containsKey(clusterID)) {
|
||||
_logger.warning('missing summary for $clusterID');
|
||||
personClusters.remove(clusterID);
|
||||
}
|
||||
}
|
||||
if (personClusters.isEmpty) {
|
||||
_logger.warning('No person clusters with summary found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the avg embedding of the person
|
||||
final w = (kDebugMode ? EnteWatch('sortSuggestions') : null)?..start();
|
||||
final personEmbeddingsCount = personClusters
|
||||
.map((e) => personClusterToSummary[e]!.$2)
|
||||
.reduce((a, b) => a + b);
|
||||
int personEmbeddingsCount = 0;
|
||||
for (final clusterID in personClusters) {
|
||||
personEmbeddingsCount += personClusterToSummary[clusterID]!.$2;
|
||||
}
|
||||
|
||||
Vector personAvg = Vector.filled(192, 0);
|
||||
for (final personClusterID in personClusters) {
|
||||
final personClusterBlob = personClusterToSummary[personClusterID]!.$1;
|
||||
@@ -1086,7 +1105,7 @@ class ClusterFeedbackService {
|
||||
final fileIdToDistanceMap = {};
|
||||
for (final entry in faceIdToVectorMap.entries) {
|
||||
fileIdToDistanceMap[getFileIdFromFaceId(entry.key)] =
|
||||
cosineDistanceSIMD(personAvg, entry.value);
|
||||
1 - personAvg.dot(entry.value);
|
||||
}
|
||||
w?.log('calculated distances for cluster $clusterID');
|
||||
suggestion.filesInCluster.sort((b, a) {
|
||||
@@ -1141,7 +1160,7 @@ List<(int, double)> _calcSuggestionsMean(Map<String, dynamic> args) {
|
||||
continue;
|
||||
}
|
||||
final Vector avg = clusterAvg[personCluster]!;
|
||||
final distance = cosineDistanceSIMD(avg, otherAvg);
|
||||
final distance = 1 - avg.dot(otherAvg);
|
||||
comparisons++;
|
||||
if (distance < maxClusterDistance) {
|
||||
if (minDistance == null || distance < minDistance) {
|
||||
|
||||
@@ -73,7 +73,7 @@ class PersonService {
|
||||
Future<void> reconcileClusters() async {
|
||||
final EnteWatch? w = kDebugMode ? EnteWatch("reconcileClusters") : null;
|
||||
w?.start();
|
||||
await storeRemoteFeedback();
|
||||
await fetchRemoteClusterFeedback();
|
||||
w?.log("Stored remote feedback");
|
||||
final dbPersonClusterInfo =
|
||||
await faceMLDataDB.getPersonToClusterIdToFaceIds();
|
||||
@@ -225,7 +225,7 @@ class PersonService {
|
||||
Bus.instance.fire(PeopleChangedEvent());
|
||||
}
|
||||
|
||||
Future<void> storeRemoteFeedback() async {
|
||||
Future<void> fetchRemoteClusterFeedback() async {
|
||||
await entityService.syncEntities();
|
||||
final entities = await entityService.getEntities(EntityType.person);
|
||||
entities.sort((a, b) => a.updatedAt.compareTo(b.updatedAt));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/db/files_db.dart";
|
||||
@@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart";
|
||||
class RemoteFileMLService {
|
||||
RemoteFileMLService._privateConstructor();
|
||||
|
||||
static final Computer _computer = Computer.shared();
|
||||
|
||||
static final RemoteFileMLService instance =
|
||||
RemoteFileMLService._privateConstructor();
|
||||
|
||||
@@ -52,13 +55,13 @@ class RemoteFileMLService {
|
||||
}
|
||||
|
||||
Future<FilesMLDataResponse> getFilessEmbedding(
|
||||
List<int> fileIds,
|
||||
Set<int> fileIds,
|
||||
) async {
|
||||
try {
|
||||
final res = await _dio.post(
|
||||
"/embeddings/files",
|
||||
data: {
|
||||
"fileIDs": fileIds,
|
||||
"fileIDs": fileIds.toList(),
|
||||
"model": 'file-ml-clip-face',
|
||||
},
|
||||
);
|
||||
@@ -107,15 +110,17 @@ class RemoteFileMLService {
|
||||
final input = EmbeddingsDecoderInput(embedding, fileKey);
|
||||
inputs.add(input);
|
||||
}
|
||||
// todo: use compute or isolate
|
||||
return decryptFileMLComputer(
|
||||
{
|
||||
return _computer.compute<Map<String, dynamic>, Map<int, FileMl>>(
|
||||
_decryptFileMLComputer,
|
||||
param: {
|
||||
"inputs": inputs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<int, FileMl>> decryptFileMLComputer(
|
||||
}
|
||||
|
||||
Future<Map<int, FileMl>> _decryptFileMLComputer(
|
||||
Map<String, dynamic> args,
|
||||
) async {
|
||||
final result = <int, FileMl>{};
|
||||
@@ -134,5 +139,4 @@ class RemoteFileMLService {
|
||||
result[input.embedding.fileID] = decodedEmbedding;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import "dart:io";
|
||||
|
||||
import "package:battery_info/battery_info_plugin.dart";
|
||||
import "package:battery_info/model/android_battery_info.dart";
|
||||
import "package:battery_info/model/iso_battery_info.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/machine_learning_control_event.dart";
|
||||
@@ -25,19 +26,27 @@ class MachineLearningController {
|
||||
bool _canRunML = false;
|
||||
late Timer _userInteractionTimer;
|
||||
|
||||
bool get isDeviceHealthy => _isDeviceHealthy;
|
||||
|
||||
void init() {
|
||||
_logger.info('init called');
|
||||
if (Platform.isAndroid) {
|
||||
_startInteractionTimer();
|
||||
BatteryInfoPlugin()
|
||||
.androidBatteryInfoStream
|
||||
.listen((AndroidBatteryInfo? batteryInfo) {
|
||||
_onBatteryStateUpdate(batteryInfo);
|
||||
_onAndroidBatteryStateUpdate(batteryInfo);
|
||||
});
|
||||
} else {
|
||||
// Always run Machine Learning on iOS
|
||||
_canRunML = true;
|
||||
Bus.instance.fire(MachineLearningControlEvent(true));
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
BatteryInfoPlugin()
|
||||
.iosBatteryInfoStream
|
||||
.listen((IosBatteryInfo? batteryInfo) {
|
||||
_oniOSBatteryStateUpdate(batteryInfo);
|
||||
});
|
||||
}
|
||||
_fireControlEvent();
|
||||
_logger.info('init done');
|
||||
}
|
||||
|
||||
void onUserInteraction() {
|
||||
@@ -53,7 +62,8 @@ class MachineLearningController {
|
||||
}
|
||||
|
||||
void _fireControlEvent() {
|
||||
final shouldRunML = _isDeviceHealthy && !_isUserInteracting;
|
||||
final shouldRunML =
|
||||
_isDeviceHealthy && (Platform.isAndroid ? !_isUserInteracting : true);
|
||||
if (shouldRunML != _canRunML) {
|
||||
_canRunML = shouldRunML;
|
||||
_logger.info(
|
||||
@@ -76,18 +86,28 @@ class MachineLearningController {
|
||||
_startInteractionTimer();
|
||||
}
|
||||
|
||||
void _onBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||
void _onAndroidBatteryStateUpdate(AndroidBatteryInfo? batteryInfo) {
|
||||
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||
_isDeviceHealthy = _computeIsDeviceHealthy(batteryInfo);
|
||||
_isDeviceHealthy = _computeIsAndroidDeviceHealthy(batteryInfo);
|
||||
_fireControlEvent();
|
||||
}
|
||||
|
||||
bool _computeIsDeviceHealthy(AndroidBatteryInfo info) {
|
||||
void _oniOSBatteryStateUpdate(IosBatteryInfo? batteryInfo) {
|
||||
_logger.info("Battery info: ${batteryInfo!.toJson()}");
|
||||
_isDeviceHealthy = _computeIsiOSDeviceHealthy(batteryInfo);
|
||||
_fireControlEvent();
|
||||
}
|
||||
|
||||
bool _computeIsAndroidDeviceHealthy(AndroidBatteryInfo info) {
|
||||
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel) &&
|
||||
_isAcceptableTemperature(info.temperature ?? kMaximumTemperature) &&
|
||||
_isBatteryHealthy(info.health ?? "");
|
||||
}
|
||||
|
||||
bool _computeIsiOSDeviceHealthy(IosBatteryInfo info) {
|
||||
return _hasSufficientBattery(info.batteryLevel ?? kMinimumBatteryLevel);
|
||||
}
|
||||
|
||||
bool _hasSufficientBattery(int batteryLevel) {
|
||||
return batteryLevel >= kMinimumBatteryLevel;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "dart:async";
|
||||
import "dart:collection";
|
||||
import "dart:io";
|
||||
import "dart:math" show min;
|
||||
|
||||
import "package:computer/computer.dart";
|
||||
@@ -24,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx
|
||||
import "package:photos/utils/debouncer.dart";
|
||||
import "package:photos/utils/device_info.dart";
|
||||
import "package:photos/utils/local_settings.dart";
|
||||
import "package:photos/utils/ml_util.dart";
|
||||
import "package:photos/utils/thumbnail_util.dart";
|
||||
|
||||
class SemanticSearchService {
|
||||
@@ -103,17 +103,13 @@ class SemanticSearchService {
|
||||
if (shouldSyncImmediately) {
|
||||
unawaited(sync());
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_startIndexing();
|
||||
}
|
||||
Bus.instance.on<MachineLearningControlEvent>().listen((event) {
|
||||
if (event.shouldRun) {
|
||||
_startIndexing();
|
||||
} else {
|
||||
_pauseIndexing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> release() async {
|
||||
@@ -165,8 +161,7 @@ class SemanticSearchService {
|
||||
}
|
||||
|
||||
Future<IndexStatus> getIndexStatus() async {
|
||||
final indexableFileIDs = await FilesDB.instance
|
||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
||||
final indexableFileIDs = await getIndexableFileIDs();
|
||||
return IndexStatus(
|
||||
min(_cachedEmbeddings.length, indexableFileIDs.length),
|
||||
(await _getFileIDsToBeIndexed()).length,
|
||||
@@ -227,8 +222,7 @@ class SemanticSearchService {
|
||||
}
|
||||
|
||||
Future<List<int>> _getFileIDsToBeIndexed() async {
|
||||
final uploadedFileIDs = await FilesDB.instance
|
||||
.getOwnedFileIDs(Configuration.instance.getUserID()!);
|
||||
final uploadedFileIDs = await getIndexableFileIDs();
|
||||
final embeddedFileIDs =
|
||||
await EmbeddingsDB.instance.getFileIDs(_currentModel);
|
||||
|
||||
|
||||