Compare commits
834 Commits
ua741-patc
...
multiselec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46db03c07b | ||
|
|
b877b0a4cc | ||
|
|
920b3b1931 | ||
|
|
ad95b5bd2d | ||
|
|
6b1757fc36 | ||
|
|
42527c0cd5 | ||
|
|
8810f88236 | ||
|
|
e1423f2030 | ||
|
|
c2ba7c56be | ||
|
|
93618117c5 | ||
|
|
08c38086a5 | ||
|
|
a4762d68f1 | ||
|
|
936c6f1b61 | ||
|
|
cfada04396 | ||
|
|
25287c64f5 | ||
|
|
168254ba42 | ||
|
|
05f7792012 | ||
|
|
d5f2b6456e | ||
|
|
ec6692b68a | ||
|
|
eead32ffe2 | ||
|
|
e90814c16e | ||
|
|
dbe0bbc9dc | ||
|
|
bbea022aef | ||
|
|
92c4b325ca | ||
|
|
bc66c1519a | ||
|
|
1e804d4829 | ||
|
|
3a8c95123e | ||
|
|
54ad3e4abb | ||
|
|
8e29a9e26b | ||
|
|
c82b829fe3 | ||
|
|
1dbdb270b4 | ||
|
|
1d1efc286f | ||
|
|
dc500795a1 | ||
|
|
11afcd92af | ||
|
|
f20c8caff0 | ||
|
|
c691b545a2 | ||
|
|
edcec3277e | ||
|
|
cda3a5b149 | ||
|
|
cc769fdd5b | ||
|
|
b74fe86e87 | ||
|
|
074f68146f | ||
|
|
e420d7b86f | ||
|
|
68caa3f7c6 | ||
|
|
5e5d5f4aad | ||
|
|
8713dd0707 | ||
|
|
102313f686 | ||
|
|
7ef9fdcaaa | ||
|
|
d902733809 | ||
|
|
0ef990de5a | ||
|
|
7722c4e16b | ||
|
|
6f5fdfb7b7 | ||
|
|
135124a487 | ||
|
|
d3c53794cf | ||
|
|
270cee8b09 | ||
|
|
9b05cc8c23 | ||
|
|
5b6c3e1b6e | ||
|
|
636793d5b1 | ||
|
|
700e52d11a | ||
|
|
82c7d1865c | ||
|
|
f08ee15cea | ||
|
|
901bfc945e | ||
|
|
6c25b094be | ||
|
|
3026ec5be3 | ||
|
|
4f5af8dcfa | ||
|
|
8079d44c68 | ||
|
|
575314c8a1 | ||
|
|
2684f9ce11 | ||
|
|
cd5582219c | ||
|
|
69332c78ad | ||
|
|
cba30e386d | ||
|
|
7663e76deb | ||
|
|
697d6f854d | ||
|
|
7aadb54ef1 | ||
|
|
2a2443efea | ||
|
|
d2bc2627a3 | ||
|
|
b1971810fb | ||
|
|
bd25af2b4b | ||
|
|
833b4656fe | ||
|
|
315c4ae6b7 | ||
|
|
49d9b3c928 | ||
|
|
440bafa56a | ||
|
|
87c236b629 | ||
|
|
ba6b326f97 | ||
|
|
aeea35e32a | ||
|
|
614c6c63aa | ||
|
|
ba6cee23d9 | ||
|
|
e43266c176 | ||
|
|
f4168cb9a3 | ||
|
|
1e551b4084 | ||
|
|
df6392fd19 | ||
|
|
e4a851072d | ||
|
|
f9c4442223 | ||
|
|
c4e7139ecb | ||
|
|
ddd4b733d3 | ||
|
|
3836cac109 | ||
|
|
06eda153be | ||
|
|
6137d07ba8 | ||
|
|
0f92b098b7 | ||
|
|
7bde215427 | ||
|
|
4953310876 | ||
|
|
2932ee7d4c | ||
|
|
0e0ba2d5af | ||
|
|
3b54fa41f6 | ||
|
|
c51dff5a29 | ||
|
|
e985200e67 | ||
|
|
7e5e11ba87 | ||
|
|
13c9646f58 | ||
|
|
678b556f5f | ||
|
|
a3b432799a | ||
|
|
8eaa2603dd | ||
|
|
b51febf8f5 | ||
|
|
df522658bb | ||
|
|
9a13b99b20 | ||
|
|
a142b660fd | ||
|
|
b7dcb7b34c | ||
|
|
e8de5940fd | ||
|
|
d5f8c9eb24 | ||
|
|
f092396133 | ||
|
|
7f718438aa | ||
|
|
cf7a4d989d | ||
|
|
e444c1801a | ||
|
|
f2a2ee188c | ||
|
|
356622cbb1 | ||
|
|
86c92a9217 | ||
|
|
bcc2a30105 | ||
|
|
dcc36d2d35 | ||
|
|
d650886749 | ||
|
|
a73d5548a0 | ||
|
|
bf0b11ebfd | ||
|
|
49c90a802a | ||
|
|
8b2db5e576 | ||
|
|
57382af3a2 | ||
|
|
80bc848d1e | ||
|
|
b11f86175e | ||
|
|
b5d4839e04 | ||
|
|
ac57097eb4 | ||
|
|
4e08e38bf6 | ||
|
|
a7d3cf4178 | ||
|
|
c63dfc36e9 | ||
|
|
2985503254 | ||
|
|
9be023d68a | ||
|
|
6a6e1b3c47 | ||
|
|
7516363715 | ||
|
|
2b76b71db8 | ||
|
|
c32a70fb25 | ||
|
|
4098c1a072 | ||
|
|
972be1f41e | ||
|
|
2e58400962 | ||
|
|
b0fce602aa | ||
|
|
3acb2136d0 | ||
|
|
eba729625f | ||
|
|
a477742cd0 | ||
|
|
c974bde11c | ||
|
|
ecc654bae0 | ||
|
|
201ef88305 | ||
|
|
742035d7cc | ||
|
|
8f29d5aa19 | ||
|
|
8a4e76fb6f | ||
|
|
c03eaf83aa | ||
|
|
378878538d | ||
|
|
01c3d6b105 | ||
|
|
2bdf62c490 | ||
|
|
c6f5c68f1e | ||
|
|
d0c8925ff3 | ||
|
|
2cab943647 | ||
|
|
d6c84421ce | ||
|
|
990485d796 | ||
|
|
96e9030d40 | ||
|
|
0d1f20f9e2 | ||
|
|
c55447a08f | ||
|
|
98d56e8fa4 | ||
|
|
f244c94ebf | ||
|
|
88f2b88f4d | ||
|
|
db1fef40db | ||
|
|
1fd29cdd13 | ||
|
|
947d294afe | ||
|
|
515715660e | ||
|
|
324221171d | ||
|
|
f5f2ff1b2c | ||
|
|
244d41621c | ||
|
|
91b6a08a35 | ||
|
|
770a311da5 | ||
|
|
db76dee639 | ||
|
|
20ce760e85 | ||
|
|
df1bfbe839 | ||
|
|
27d72eb821 | ||
|
|
98786c5824 | ||
|
|
d38a09c3f0 | ||
|
|
91785d8c90 | ||
|
|
b1f28e3f2e | ||
|
|
c155bdd058 | ||
|
|
a859f28e2c | ||
|
|
8d75528aa5 | ||
|
|
7f43c11985 | ||
|
|
aadda7e3f6 | ||
|
|
210c18d244 | ||
|
|
6636849838 | ||
|
|
5500315351 | ||
|
|
562292e642 | ||
|
|
4aa80edbcf | ||
|
|
9524a639cd | ||
|
|
b8eb793c16 | ||
|
|
4b514f1e1a | ||
|
|
bee2bb9621 | ||
|
|
772121c22e | ||
|
|
3c49ca0f6e | ||
|
|
f2e51893ad | ||
|
|
c08b78c775 | ||
|
|
233f03355f | ||
|
|
73ab50f113 | ||
|
|
4a2346fe93 | ||
|
|
68b5cce158 | ||
|
|
e907a9e8cb | ||
|
|
92a40afca2 | ||
|
|
0c2b38c059 | ||
|
|
19650bcd57 | ||
|
|
2b9ca073ce | ||
|
|
2257087bb2 | ||
|
|
2a5bce2ae4 | ||
|
|
1e0a6eb1ea | ||
|
|
187a729013 | ||
|
|
c98f4dfffd | ||
|
|
4140a0f6fe | ||
|
|
cf4b87dad9 | ||
|
|
3fd0db6a90 | ||
|
|
a9d5773b9a | ||
|
|
ac68b99ecf | ||
|
|
82e1a0e358 | ||
|
|
ce1701d211 | ||
|
|
034e789242 | ||
|
|
ccfec4071f | ||
|
|
c4830732fd | ||
|
|
72dc56e41f | ||
|
|
8dd3ad9f5b | ||
|
|
2ebb920faa | ||
|
|
e9f55b968a | ||
|
|
5036a8da59 | ||
|
|
aaed336991 | ||
|
|
0b85dfe7e4 | ||
|
|
68422b172f | ||
|
|
db99dae3e1 | ||
|
|
3717a156d3 | ||
|
|
ca9930e01b | ||
|
|
eb23a4e770 | ||
|
|
e03303e5b3 | ||
|
|
2ad27f1c6e | ||
|
|
202e6a9f7c | ||
|
|
ceaedad327 | ||
|
|
fd963a1c8e | ||
|
|
b40b5bb1ae | ||
|
|
91827626b2 | ||
|
|
42318335ae | ||
|
|
858db62385 | ||
|
|
46e36612d3 | ||
|
|
62cf236e3b | ||
|
|
c2b1ab86f2 | ||
|
|
43adf42281 | ||
|
|
1e2a65281c | ||
|
|
70eb68b13c | ||
|
|
fa86b19307 | ||
|
|
e632dc7771 | ||
|
|
7fa9adb636 | ||
|
|
83f885f158 | ||
|
|
a295f223b6 | ||
|
|
6775faf0d0 | ||
|
|
cc64ef8035 | ||
|
|
69dd7b6233 | ||
|
|
367dc18caa | ||
|
|
0c6db4661e | ||
|
|
bcc9f1be73 | ||
|
|
296b2a2a6c | ||
|
|
6b48c9bc34 | ||
|
|
6a951bcc72 | ||
|
|
38914981a1 | ||
|
|
66f4d5b1a6 | ||
|
|
9ee3781320 | ||
|
|
907d1d2bb8 | ||
|
|
8218283463 | ||
|
|
bd43385949 | ||
|
|
2e6a9acaf9 | ||
|
|
a02dcace7d | ||
|
|
cf4285de6d | ||
|
|
f831491e4a | ||
|
|
af154d82de | ||
|
|
ff2f75ea74 | ||
|
|
97e3ef819a | ||
|
|
3685cd2154 | ||
|
|
c64fff8ca4 | ||
|
|
23dc809589 | ||
|
|
33d1242c6d | ||
|
|
b8ee9fafd1 | ||
|
|
f72c9fa068 | ||
|
|
1a7275a101 | ||
|
|
fa7ccbd180 | ||
|
|
79e26d6993 | ||
|
|
023135afb5 | ||
|
|
04aaa3a5e4 | ||
|
|
848857f409 | ||
|
|
137033be67 | ||
|
|
b6489f4c41 | ||
|
|
e7d7f1cdd0 | ||
|
|
bbbdd96c9e | ||
|
|
3c23d3b480 | ||
|
|
3805cddeba | ||
|
|
824c324342 | ||
|
|
04b6f4a765 | ||
|
|
2645ba0949 | ||
|
|
5958647fa8 | ||
|
|
b7b91631f6 | ||
|
|
67d7f586b2 | ||
|
|
7c22a8bb25 | ||
|
|
ff3864a09a | ||
|
|
4484b9e4ad | ||
|
|
e9554ffbcb | ||
|
|
ad3901d484 | ||
|
|
ecca4c3dc8 | ||
|
|
d05521f884 | ||
|
|
ff37c4bf81 | ||
|
|
446df755fa | ||
|
|
0f5e30e96b | ||
|
|
35ded7bc59 | ||
|
|
a7805784b7 | ||
|
|
8e3f6e56d2 | ||
|
|
6ded21fe87 | ||
|
|
be4b521879 | ||
|
|
326eb3ff8a | ||
|
|
adef8bd466 | ||
|
|
a1d9fb5969 | ||
|
|
6da615b7dc | ||
|
|
41a268b1cb | ||
|
|
ed07e64fa5 | ||
|
|
84a5ad0b86 | ||
|
|
44ad11343a | ||
|
|
07e50e3cfe | ||
|
|
df8bbdb788 | ||
|
|
1ed381fe52 | ||
|
|
55090436ce | ||
|
|
150534aa1a | ||
|
|
ddd1d5ac86 | ||
|
|
26845a502e | ||
|
|
21aac29020 | ||
|
|
bdfe363066 | ||
|
|
c1ff02df14 | ||
|
|
e4927c4022 | ||
|
|
4fd797338b | ||
|
|
eca0e5943d | ||
|
|
56cc7309a5 | ||
|
|
b740d1af05 | ||
|
|
6d21b73367 | ||
|
|
a5704eef25 | ||
|
|
7e83682686 | ||
|
|
18d5aa61b0 | ||
|
|
7c2a719ba8 | ||
|
|
2a136ba087 | ||
|
|
47313a74ff | ||
|
|
3abb479fbf | ||
|
|
7eda60a493 | ||
|
|
bb8c5caa8d | ||
|
|
65a7a16298 | ||
|
|
9251e4f5b6 | ||
|
|
c4bc6abf83 | ||
|
|
0384819c01 | ||
|
|
f55973367d | ||
|
|
3165289483 | ||
|
|
01aab41c25 | ||
|
|
1826258161 | ||
|
|
df5917060b | ||
|
|
b5aa05cc1b | ||
|
|
cd865992f2 | ||
|
|
370c0ab54a | ||
|
|
699794226f | ||
|
|
dee68acfc3 | ||
|
|
0bd5452837 | ||
|
|
923f2484fb | ||
|
|
37928cd2c6 | ||
|
|
fc32ba97c1 | ||
|
|
e49084867e | ||
|
|
e53ddb8b51 | ||
|
|
a046748ded | ||
|
|
047d708ef1 | ||
|
|
95d167878e | ||
|
|
5b5f563d47 | ||
|
|
2b60ad3748 | ||
|
|
1f70043c83 | ||
|
|
7ce6f6a346 | ||
|
|
653fc47aed | ||
|
|
03814bff0c | ||
|
|
34325691e7 | ||
|
|
4c63a0ff13 | ||
|
|
e474114e22 | ||
|
|
93552fb872 | ||
|
|
1b61becdcf | ||
|
|
80c07d36a9 | ||
|
|
8581742a73 | ||
|
|
042dae8790 | ||
|
|
0499cad3c9 | ||
|
|
79752ef4b8 | ||
|
|
c1bd6d3fdb | ||
|
|
621423d9a4 | ||
|
|
edb11c89ba | ||
|
|
adb71fe09c | ||
|
|
c20cee2406 | ||
|
|
dcfad86c47 | ||
|
|
0a2bff67bf | ||
|
|
7aaa689cfb | ||
|
|
ad2a0ce897 | ||
|
|
d99615b24f | ||
|
|
09cc48ae55 | ||
|
|
6ab2223a80 | ||
|
|
6fd86162e0 | ||
|
|
707e8dbfcf | ||
|
|
5869bec781 | ||
|
|
45249e0cdf | ||
|
|
ebfcedac7b | ||
|
|
e311a8bb32 | ||
|
|
547ccfceca | ||
|
|
2900ca55f5 | ||
|
|
2a40aa472e | ||
|
|
3a1917949b | ||
|
|
3a1ce3258e | ||
|
|
13b2542bea | ||
|
|
6db3741a3b | ||
|
|
62cb67f3bf | ||
|
|
e393b92a3d | ||
|
|
e06d65e8a0 | ||
|
|
a4ec8c939a | ||
|
|
b8dd379306 | ||
|
|
42229bd331 | ||
|
|
ad9a3977a3 | ||
|
|
afb93df48f | ||
|
|
4ce38ecea0 | ||
|
|
4c63c8fc25 | ||
|
|
ce17eccd68 | ||
|
|
95dc683088 | ||
|
|
cf9d5f72f7 | ||
|
|
3096e1550a | ||
|
|
1b39435735 | ||
|
|
8f3d8505bb | ||
|
|
47e8aafe25 | ||
|
|
bc6506cb10 | ||
|
|
edf32d065e | ||
|
|
1fa6a0c3b9 | ||
|
|
f2a26ba391 | ||
|
|
2388989dd0 | ||
|
|
9e392277b1 | ||
|
|
4609c375db | ||
|
|
839c62ea72 | ||
|
|
dceef49f33 | ||
|
|
acbdc3111a | ||
|
|
98b91a6935 | ||
|
|
e1640e67d4 | ||
|
|
e875758419 | ||
|
|
214b120472 | ||
|
|
f139e0a098 | ||
|
|
e3c9a61887 | ||
|
|
0da3dc5084 | ||
|
|
a856a82249 | ||
|
|
fbdec00a62 | ||
|
|
6a7f980a0d | ||
|
|
10a855fe27 | ||
|
|
b4f8a2b27c | ||
|
|
89489b4d7c | ||
|
|
158b48e4dc | ||
|
|
50296f8dfa | ||
|
|
fe732f2778 | ||
|
|
ca8a067966 | ||
|
|
f69cec864b | ||
|
|
73d5d33fc5 | ||
|
|
4d8ea12ddd | ||
|
|
7beb267ba7 | ||
|
|
7e13ef3537 | ||
|
|
47edca5bf5 | ||
|
|
925ba10b15 | ||
|
|
db2d0bb7e9 | ||
|
|
f3a2b2af0c | ||
|
|
967e88f88d | ||
|
|
b44734a493 | ||
|
|
6478b08a19 | ||
|
|
314e81565b | ||
|
|
f95e20d00f | ||
|
|
35a04d6e7e | ||
|
|
403264d2c9 | ||
|
|
84f5a5ac3d | ||
|
|
6b06a4c388 | ||
|
|
678bce89b2 | ||
|
|
a00fc0b1be | ||
|
|
f5347e7436 | ||
|
|
3f1d574d0c | ||
|
|
891b68c0f4 | ||
|
|
f050c6f9d7 | ||
|
|
2de67b619f | ||
|
|
828dde5ca7 | ||
|
|
2526c69896 | ||
|
|
2f1d4b9f1a | ||
|
|
af20eadff0 | ||
|
|
6e64a2067f | ||
|
|
ab4792518f | ||
|
|
d4ae8d63fc | ||
|
|
3264ea046c | ||
|
|
618753cb1a | ||
|
|
d81a73c833 | ||
|
|
ac9c63fe29 | ||
|
|
53cb217dbc | ||
|
|
f84bd20bbf | ||
|
|
fca9a42e0a | ||
|
|
5e3a779925 | ||
|
|
d1b06abada | ||
|
|
8b708228be | ||
|
|
d379262f56 | ||
|
|
6ae7aa70d6 | ||
|
|
48757af5d0 | ||
|
|
9282632af1 | ||
|
|
6a43d6a567 | ||
|
|
1cdbef1a01 | ||
|
|
fa84bb0845 | ||
|
|
cd20a98850 | ||
|
|
cbb6f07d0d | ||
|
|
9ac9e6bd26 | ||
|
|
fad9cf8559 | ||
|
|
371ba9c552 | ||
|
|
19086e43cc | ||
|
|
964c837c40 | ||
|
|
d85121862d | ||
|
|
0b640c9062 | ||
|
|
2d87aba165 | ||
|
|
7dffdfaecf | ||
|
|
a4da7b5555 | ||
|
|
42d31a73a3 | ||
|
|
946d2ae522 | ||
|
|
8e9eb50783 | ||
|
|
af3bc7757f | ||
|
|
eda1d05216 | ||
|
|
b58e0f8331 | ||
|
|
6dcf53650d | ||
|
|
bff53d9081 | ||
|
|
f3306e14c7 | ||
|
|
b5c075bac4 | ||
|
|
241d21c2aa | ||
|
|
789d77747c | ||
|
|
35050aa32f | ||
|
|
40e6bd9fae | ||
|
|
9fe15d7ff0 | ||
|
|
28a2afe275 | ||
|
|
c072097c11 | ||
|
|
b317df2000 | ||
|
|
dd420a80a4 | ||
|
|
3dc0620e18 | ||
|
|
173d075f8b | ||
|
|
48283282e5 | ||
|
|
fa555c448f | ||
|
|
6f6770d677 | ||
|
|
0b894e9724 | ||
|
|
0670550cb1 | ||
|
|
45783cf527 | ||
|
|
1615779eb8 | ||
|
|
02e4c9d8fd | ||
|
|
2e706228ee | ||
|
|
6eab6457ee | ||
|
|
25490a7238 | ||
|
|
85b766b5d0 | ||
|
|
62f715d3c1 | ||
|
|
f519ff8a51 | ||
|
|
afebe1ade1 | ||
|
|
3862644dd5 | ||
|
|
274a7d207d | ||
|
|
add2f0c8de | ||
|
|
8e807616e0 | ||
|
|
70f4325c71 | ||
|
|
38ea2248b8 | ||
|
|
9600b26359 | ||
|
|
5b3e996aaa | ||
|
|
4d4cce091f | ||
|
|
aaca140d1b | ||
|
|
596ffcd4c4 | ||
|
|
41ef85a294 | ||
|
|
f722d82835 | ||
|
|
cbb3096534 | ||
|
|
e35ae86fa5 | ||
|
|
ea843eba7a | ||
|
|
f635e1e856 | ||
|
|
c6734a5cb7 | ||
|
|
e26b4796d3 | ||
|
|
99c0194c0f | ||
|
|
e824c02d7f | ||
|
|
a11f66b51d | ||
|
|
f202fef266 | ||
|
|
ff8cfd3e87 | ||
|
|
431ab7fcc7 | ||
|
|
b845f4d893 | ||
|
|
8ea36acb7a | ||
|
|
2ac1d58dac | ||
|
|
5533e6a71d | ||
|
|
a8ae0727a8 | ||
|
|
6955788724 | ||
|
|
f6dd35f5e7 | ||
|
|
9148916d88 | ||
|
|
68545f8947 | ||
|
|
fe40185889 | ||
|
|
6bb4428a8a | ||
|
|
76c5c12c53 | ||
|
|
0881685915 | ||
|
|
3e13932d03 | ||
|
|
8d749a2dc8 | ||
|
|
56af818482 | ||
|
|
fc1096c985 | ||
|
|
88260a05e3 | ||
|
|
4b5f91a428 | ||
|
|
29b12fc6b5 | ||
|
|
c3eec71d60 | ||
|
|
2fccdee0d6 | ||
|
|
8f053e7a7b | ||
|
|
de7291f5d4 | ||
|
|
e09c952198 | ||
|
|
28a842b006 | ||
|
|
19eb342f59 | ||
|
|
2b82c79be9 | ||
|
|
86a9dee49d | ||
|
|
33b1a7e4f8 | ||
|
|
de783e91dc | ||
|
|
554c8f4b7a | ||
|
|
f438142646 | ||
|
|
aa6c010562 | ||
|
|
2830c89bde | ||
|
|
4de530b882 | ||
|
|
62d7c87dc7 | ||
|
|
3a1b6dbf15 | ||
|
|
58182cc8ab | ||
|
|
5e0bba390b | ||
|
|
9e70dc4312 | ||
|
|
df6a3b94db | ||
|
|
72b7e12768 | ||
|
|
541d71f65c | ||
|
|
3a7d82a799 | ||
|
|
5f1cfb9ba5 | ||
|
|
298e3695c7 | ||
|
|
621713d0b4 | ||
|
|
34813d2fae | ||
|
|
bb177bc3f6 | ||
|
|
d8e4418d78 | ||
|
|
9771a5bc5d | ||
|
|
65f7e3f6c6 | ||
|
|
49b9d83f05 | ||
|
|
273d7bd00a | ||
|
|
4e8991dc10 | ||
|
|
aa4207f878 | ||
|
|
3176ba8a93 | ||
|
|
fcf038c4d8 | ||
|
|
46aad76039 | ||
|
|
9a654988f8 | ||
|
|
1ce749e93e | ||
|
|
db88432b9d | ||
|
|
354bcc715f | ||
|
|
5541198967 | ||
|
|
3e2dbe2c1b | ||
|
|
5d3d18f347 | ||
|
|
9a876f3d59 | ||
|
|
3b7600ae7b | ||
|
|
8bacf8ff93 | ||
|
|
b356e5d0a5 | ||
|
|
777516446d | ||
|
|
37c1d0f6a8 | ||
|
|
be1bf28cd8 | ||
|
|
de5f0fbb39 | ||
|
|
48fa2bd6de | ||
|
|
df584f34e9 | ||
|
|
0241ad1fe5 | ||
|
|
435ed212c6 | ||
|
|
1e9a6a1e14 | ||
|
|
a7ba3079cb | ||
|
|
97f1bb71c7 | ||
|
|
cad8613e81 | ||
|
|
b46e51f64d | ||
|
|
e6bf64548c | ||
|
|
5729e0cf3e | ||
|
|
b353539328 | ||
|
|
f2049ac7fa | ||
|
|
e82ba882d6 | ||
|
|
eecd7ed355 | ||
|
|
bc70b4e725 | ||
|
|
a52cebf0e5 | ||
|
|
4c31a7bcd6 | ||
|
|
d8fc369a21 | ||
|
|
8efbebe9c4 | ||
|
|
cf938eca91 | ||
|
|
deef13ece9 | ||
|
|
a3d3ee24f8 | ||
|
|
6b37cc46a5 | ||
|
|
3132373c26 | ||
|
|
5b4ff1d01a | ||
|
|
a7300b7ac7 | ||
|
|
20d8a42239 | ||
|
|
b19814dd2c | ||
|
|
ad39694026 | ||
|
|
9224cea96f | ||
|
|
72ede1a109 | ||
|
|
9fbc618d69 | ||
|
|
4614428f76 | ||
|
|
6fde4ee45f | ||
|
|
279df8ff57 | ||
|
|
d83994c692 | ||
|
|
5fe86858ef | ||
|
|
7546d8cad2 | ||
|
|
a74908214d | ||
|
|
be506bdad1 | ||
|
|
94e398dc89 | ||
|
|
a5a19581fc | ||
|
|
ea409fc266 | ||
|
|
0e9d7106f7 | ||
|
|
6fe89fdc0e | ||
|
|
193e1374e1 | ||
|
|
e11a6ace80 | ||
|
|
9033bd8cec | ||
|
|
085551b5a7 | ||
|
|
210a0a45c1 | ||
|
|
ee035681ab | ||
|
|
cc2d65d796 | ||
|
|
0e61b3dfd4 | ||
|
|
0a3035e5d5 | ||
|
|
7adb1c0a6c | ||
|
|
cb55be1e5c | ||
|
|
d7a7144b33 | ||
|
|
e6a867a859 | ||
|
|
a9c8e4476f | ||
|
|
f914263b2f | ||
|
|
6f94d91afb | ||
|
|
57569e79fe | ||
|
|
b695db80ab | ||
|
|
ae1a43b8bf | ||
|
|
d9d9acfa3e | ||
|
|
d430936ae8 | ||
|
|
b02acc579f | ||
|
|
e87be4b9af | ||
|
|
399148aa59 | ||
|
|
abd733934b | ||
|
|
661e1f92d5 | ||
|
|
97b36681dc | ||
|
|
5dd5756a41 | ||
|
|
d2cfa374bd | ||
|
|
fb64c8aa4c | ||
|
|
86b54e2241 | ||
|
|
c33396ea60 | ||
|
|
24f48b5054 | ||
|
|
094c92c8b6 | ||
|
|
c2922a0cb2 | ||
|
|
9a357a716d | ||
|
|
5574fd748e | ||
|
|
f2be25667f | ||
|
|
053d0cfcaa | ||
|
|
0ff8184f47 | ||
|
|
f971835ae7 | ||
|
|
694e3ca121 | ||
|
|
be850c27c6 | ||
|
|
3aaf11ba1d | ||
|
|
3019d858c9 | ||
|
|
81e926ef2d | ||
|
|
016d646971 | ||
|
|
1f100566ad | ||
|
|
d69595e744 | ||
|
|
7a91e714fe | ||
|
|
e352de8b9c | ||
|
|
e36831b599 | ||
|
|
df74c7d54d | ||
|
|
d7986b5c7c | ||
|
|
3e888876d1 | ||
|
|
6af494206e | ||
|
|
93f32de8c1 | ||
|
|
e0b3e6464e | ||
|
|
c67d2f0836 | ||
|
|
49106a3dd9 | ||
|
|
b61c75dc84 | ||
|
|
465fc7c7d3 | ||
|
|
487e4ef559 | ||
|
|
34bf4f6bba | ||
|
|
af36978ede | ||
|
|
a375dfdc2e | ||
|
|
a5b0e66e9d | ||
|
|
2b3b7a5e2a | ||
|
|
b1dc9272a0 | ||
|
|
7c33c160b2 | ||
|
|
be232efbc6 | ||
|
|
bbac09b4a6 | ||
|
|
a4626ae7a1 | ||
|
|
e8551df8b9 | ||
|
|
77cb40aef4 | ||
|
|
827090fb24 | ||
|
|
39b9670fcc | ||
|
|
d227a2bf20 | ||
|
|
0b766415a4 | ||
|
|
3b727549d5 | ||
|
|
dbdf19ee8d | ||
|
|
2e3d621077 | ||
|
|
0455481f3d | ||
|
|
c9a7918397 | ||
|
|
957f0bc041 | ||
|
|
24f5a5813a | ||
|
|
860b2895f6 | ||
|
|
a510320d0e | ||
|
|
de04f18cb2 | ||
|
|
b84b73fda2 | ||
|
|
12b0618149 | ||
|
|
28a43393f9 | ||
|
|
21b3bdf204 | ||
|
|
3436fb7fb1 | ||
|
|
045b40b2b2 | ||
|
|
c09922d1a3 | ||
|
|
3e9032588e | ||
|
|
14e570b676 | ||
|
|
a412aa4886 | ||
|
|
a7a162d375 | ||
|
|
b444bdc5ec | ||
|
|
f9299e7950 | ||
|
|
8a9f73ada5 | ||
|
|
d5c1970ca2 | ||
|
|
f8aff3e12b | ||
|
|
490759243b | ||
|
|
e8a9e509a8 | ||
|
|
c392ad5dcb | ||
|
|
9bb084d610 | ||
|
|
00b05e2d7c | ||
|
|
93736fe57a | ||
|
|
995ae50418 | ||
|
|
42e6dff0f5 | ||
|
|
150513d3e5 | ||
|
|
13ed1e76bc | ||
|
|
a7d0e2eef5 | ||
|
|
134314c285 | ||
|
|
eb3e3db8e6 | ||
|
|
3b9b886ae9 | ||
|
|
d88df36c3d | ||
|
|
62d7311780 | ||
|
|
9d76ccc173 | ||
|
|
1752192688 | ||
|
|
873ee3ac14 | ||
|
|
cfce2d00f5 |
42
.github/workflows/mobile-daily-internal.yml
vendored
42
.github/workflows/mobile-daily-internal.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.32.8"
|
||||
RUST_VERSION: "1.85.1"
|
||||
RUST_VERSION: "1.86.0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -27,6 +27,38 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
echo "Initial disk usage:"
|
||||
df -h /
|
||||
# Get available space in KB
|
||||
INITIAL=$(df / | awk 'NR==2 {print $4}')
|
||||
|
||||
echo -e "\n=== Removing .NET SDK (~20-25GB) ==="
|
||||
BEFORE=$(df / | awk 'NR==2 {print $4}')
|
||||
START=$(date +%s)
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
END=$(date +%s)
|
||||
AFTER=$(df / | awk 'NR==2 {print $4}')
|
||||
FREED=$(( (AFTER - BEFORE) / 1048576 )) # Convert KB to GB
|
||||
echo "Time: $((END-START))s | Freed: ${FREED}GB"
|
||||
|
||||
echo -e "\n=== Removing cached tools (~5-10GB) ==="
|
||||
BEFORE=$(df / | awk 'NR==2 {print $4}')
|
||||
START=$(date +%s)
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
END=$(date +%s)
|
||||
AFTER=$(df / | awk 'NR==2 {print $4}')
|
||||
FREED=$(( (AFTER - BEFORE) / 1048576 ))
|
||||
echo "Time: $((END-START))s | Freed: ${FREED}GB"
|
||||
|
||||
echo -e "\n=== Final Summary ==="
|
||||
FINAL=$(df / | awk 'NR==2 {print $4}')
|
||||
TOTAL_FREED=$(( (FINAL - INITIAL) / 1048576 ))
|
||||
echo "Total space freed: ${TOTAL_FREED}GB"
|
||||
echo "Final disk usage:"
|
||||
df -h /
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
@@ -39,14 +71,12 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust ${{ env.RUST_VERSION }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Install Flutter Rust Bridge
|
||||
run: cargo install flutter_rust_bridge_codegen
|
||||
|
||||
- name: Generate Rust bindings
|
||||
run: flutter_rust_bridge_codegen generate
|
||||
|
||||
- name: Increment version code for build
|
||||
run: |
|
||||
CURRENT_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //')
|
||||
|
||||
77
.github/workflows/mobile-internal-release.yml
vendored
77
.github/workflows/mobile-internal-release.yml
vendored
@@ -1,77 +0,0 @@
|
||||
name: "Old Internal release (photos)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.32.8"
|
||||
RUST_VERSION: "1.85.1"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile/apps/photos
|
||||
|
||||
steps:
|
||||
- name: Checkout code and submodules
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
|
||||
- name: Install Flutter ${{ env.FLUTTER_VERSION }}
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Rust ${{ env.RUST_VERSION }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Install Flutter Rust Bridge
|
||||
run: cargo install flutter_rust_bridge_codegen
|
||||
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
fileName: "keystore/ente_photos_key.jks"
|
||||
encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }}
|
||||
|
||||
- name: Build PlayStore AAB
|
||||
run: |
|
||||
flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD_PHOTOS }}
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
|
||||
|
||||
- name: Upload AAB to PlayStore
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
packageName: io.ente.photos
|
||||
releaseFiles: mobile/apps/photos/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab
|
||||
track: internal
|
||||
|
||||
- name: Notify Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release Photos (Branch: ${{ github.ref_name }})"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)"
|
||||
color: 0x00ff00
|
||||
14
.github/workflows/mobile-lint.yml
vendored
14
.github/workflows/mobile-lint.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.32.8"
|
||||
RUST_VERSION: "1.86.0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -31,7 +32,18 @@ jobs:
|
||||
channel: "stable"
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
|
||||
- run: flutter pub get
|
||||
|
||||
- name: Install Rust ${{ env.RUST_VERSION }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Install Flutter Rust Bridge
|
||||
run: cargo install flutter_rust_bridge_codegen
|
||||
|
||||
- name: Generate Rust bindings
|
||||
run: flutter_rust_bridge_codegen generate
|
||||
|
||||
- run: flutter analyze --no-fatal-infos
|
||||
|
||||
38
.github/workflows/mobile-release.yml
vendored
38
.github/workflows/mobile-release.yml
vendored
@@ -28,6 +28,38 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Free up disk space
|
||||
run: |
|
||||
echo "Initial disk usage:"
|
||||
df -h /
|
||||
# Get available space in KB
|
||||
INITIAL=$(df / | awk 'NR==2 {print $4}')
|
||||
|
||||
echo -e "\n=== Removing .NET SDK (~20-25GB) ==="
|
||||
BEFORE=$(df / | awk 'NR==2 {print $4}')
|
||||
START=$(date +%s)
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
END=$(date +%s)
|
||||
AFTER=$(df / | awk 'NR==2 {print $4}')
|
||||
FREED=$(( (AFTER - BEFORE) / 1048576 )) # Convert KB to GB
|
||||
echo "Time: $((END-START))s | Freed: ${FREED}GB"
|
||||
|
||||
echo -e "\n=== Removing cached tools (~5-10GB) ==="
|
||||
BEFORE=$(df / | awk 'NR==2 {print $4}')
|
||||
START=$(date +%s)
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
END=$(date +%s)
|
||||
AFTER=$(df / | awk 'NR==2 {print $4}')
|
||||
FREED=$(( (AFTER - BEFORE) / 1048576 ))
|
||||
echo "Time: $((END-START))s | Freed: ${FREED}GB"
|
||||
|
||||
echo -e "\n=== Final Summary ==="
|
||||
FINAL=$(df / | awk 'NR==2 {print $4}')
|
||||
TOTAL_FREED=$(( (FINAL - INITIAL) / 1048576 ))
|
||||
echo "Total space freed: ${TOTAL_FREED}GB"
|
||||
echo "Final disk usage:"
|
||||
df -h /
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
@@ -40,6 +72,12 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
cache: true
|
||||
|
||||
- name: Install Flutter Rust Bridge
|
||||
run: cargo install flutter_rust_bridge_codegen
|
||||
|
||||
- name: Generate Rust bindings
|
||||
run: flutter_rust_bridge_codegen generate
|
||||
|
||||
- name: Setup keys
|
||||
uses: timheuer/base64-to-file@v1
|
||||
with:
|
||||
|
||||
4
.github/workflows/web-deploy-one.yml
vendored
4
.github/workflows/web-deploy-one.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
@@ -38,7 +40,7 @@ jobs:
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build ${{ inputs.app }}
|
||||
run: yarn build:${{ inputs.app }}
|
||||
|
||||
4
.github/workflows/web-deploy-preview.yml
vendored
4
.github/workflows/web-deploy-preview.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
@@ -38,7 +40,7 @@ jobs:
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build ${{ inputs.app }}
|
||||
run: yarn build:${{ inputs.app }}
|
||||
|
||||
3
.github/workflows/web-deploy-staging.yml
vendored
3
.github/workflows/web-deploy-staging.yml
vendored
@@ -37,6 +37,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.select-branch.outputs.branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build photos
|
||||
run: yarn build:photos
|
||||
|
||||
12
.github/workflows/web-deploy.yml
vendored
12
.github/workflows/web-deploy.yml
vendored
@@ -33,6 +33,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
@@ -42,7 +44,15 @@ jobs:
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Audit dependencies
|
||||
run: |
|
||||
yarn audit --level critical || exit_code=$?
|
||||
if [[ $exit_code -ge 16 ]]; then
|
||||
echo "::error::Yarn audit found critical issues"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build photos
|
||||
run: yarn build:photos
|
||||
|
||||
12
.github/workflows/web-lint.yml
vendored
12
.github/workflows/web-lint.yml
vendored
@@ -24,6 +24,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
@@ -32,6 +34,14 @@ jobs:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- run: yarn install
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn lint
|
||||
|
||||
- name: Audit dependencies
|
||||
run: |
|
||||
yarn audit --level critical || exit_code=$?
|
||||
if [[ $exit_code -ge 16 ]]; then
|
||||
echo "::error::Yarn audit found critical issues"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -48,7 +48,11 @@ See [docs/](docs/README.md) for how to edit these documents.
|
||||
|
||||
## Code contributions
|
||||
|
||||
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug.
|
||||
If you'd like to contribute code, it is best to start small. Consider some well-scoped changes, say like adding more [custom icons to auth](mobile/apps/auth/docs/adding-icons.md), or fixing a specific bug. There is a (possibly outdated) list of tasks with the ["help wanted" or "good first issue"](<https://github.com/ente-io/ente/issues?q=state%3Aopen%20(label%3A%22good%20first%20issue%22%20OR%20label%3A%22help%20wanted%22%20)>) label too.
|
||||
|
||||
If you use any form of AI assistance, please include a co-author attribution in the commit for transparency.
|
||||
|
||||
In your PR, please include before / after screenshots, and clearly indicate the tests that you performed.
|
||||
|
||||
Code that changes the behaviour of the product might not get merged, at least not initially. The PR can serve as a discussion bed, but you might find it easier to just start a discussion instead, or post your perspective in the (likely) existing thread about the behaviour change or new feature you wish for.
|
||||
|
||||
|
||||
@@ -142,6 +142,22 @@ var _updateFreeUserStorage = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var _sendMail = &cobra.Command{
|
||||
Use: "send-mail <to-email> <from-email> <from-name>",
|
||||
Args: cobra.ExactArgs(3),
|
||||
Short: "Sends a test mail via the admin api",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
recoverWithLog()
|
||||
var flags = &model.AdminActionForUser{}
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "admin-user" {
|
||||
flags.AdminEmail = f.Value.String()
|
||||
}
|
||||
})
|
||||
return ctrl.SendTestMail(context.Background(), *flags, args[0], args[1], args[2])
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(_adminCmd)
|
||||
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
|
||||
@@ -159,5 +175,6 @@ func init() {
|
||||
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
|
||||
// add a flag with no value --no-limit
|
||||
_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser)
|
||||
_sendMail.Flags().StringP("admin-user", "a", "", "The email of the admin user. ")
|
||||
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser, _sendMail)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/pkg"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/ente-io/cli/pkg"
|
||||
"github.com/spf13/cobra/doc"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -20,11 +21,6 @@ var ctrl *pkg.ClICtrl
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ente",
|
||||
Short: "CLI tool for exporting your photos from ente.io",
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Sprintf("Hello World")
|
||||
},
|
||||
}
|
||||
|
||||
func GenerateDocs() error {
|
||||
|
||||
@@ -139,5 +139,28 @@ func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.User
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) SendTestMail(ctx context.Context, toEmail, fromEmail, fromName string) error {
|
||||
body := map[string]interface{}{
|
||||
"to": []string{toEmail},
|
||||
"fromName": fromName,
|
||||
"fromEmail": fromEmail,
|
||||
"subject": "Test mail from Ente",
|
||||
"body": "This is a test mail from Ente",
|
||||
}
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
SetBody(body).
|
||||
Post("/admin/mail")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.IsError() {
|
||||
return &ApiError{
|
||||
StatusCode: r.StatusCode(),
|
||||
Message: r.String(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,6 +156,23 @@ func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActio
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) SendTestMail(ctx context.Context, params model.AdminActionForUser, to, from, fromName string) error {
|
||||
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.Client.SendTestMail(accountCtx, to, from, fromName)
|
||||
if err != nil {
|
||||
if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") {
|
||||
fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Successfully sent test email to %s\n", to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
|
||||
accounts, err := c.GetAccounts(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## v1.7.15 (Unreleased)
|
||||
|
||||
- Custom domains.
|
||||
- Support Czech translations.
|
||||
- .
|
||||
|
||||
## v1.7.14
|
||||
|
||||
0
docs/docs/cli/index.md
Normal file
0
docs/docs/cli/index.md
Normal file
@@ -44,7 +44,7 @@ The first step is to let Ente know about the domain or subdomain you wish to use
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Currently (Aug 2025) the ability to link a custom domain is only present in Ente's web app, [web.ente.io](https://web.ente.io). It will come to Ente mobile and desktop when their next versions get released.
|
||||
> Currently (Sep 2025) the ability to link a custom domain is only present in Ente's web app, [web.ente.io](https://web.ente.io).
|
||||
|
||||
Head over to Preferences > Custom domains, in the domain field enter "pics.example.org" (replace with your subdomain) and press "Save". That's it. The linking is done.
|
||||
|
||||
@@ -94,7 +94,7 @@ Using is trivial. When you go to an album's sharing options and copy the link to
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> Currently (Aug 2025) the ability to automatically substitute your custom domain is only present in Ente's web app, [web.ente.io](https://web.ente.io). It will come to Ente mobile and desktop when their next versions get released.
|
||||
> Currently (Sep 2025) the ability to automatically substitute your custom domain is present in Ente's web and mobile apps, but not in the desktop app (The next desktop version to be released will have that ability too).
|
||||
|
||||
## Unsetting
|
||||
|
||||
@@ -103,3 +103,7 @@ To stop using your custom domain, we need to undo the two steps we did during se
|
||||
1. Unlink your domain in Ente. This can be done just by going to Preferences > Custom Domains, clearing the value in the "Domain" input and pressing "Update".
|
||||
|
||||
2. Remove the CNAME record you added during setup in your DNS provider.
|
||||
|
||||
## Implementation
|
||||
|
||||
Our engineers also wrote [explainer](https://ente.io/blog/custom-domains/) of how this works behind the scenes.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Removing duplicates photos using Ente Photos
|
||||
# Deduplicate
|
||||
|
||||
Ente performs two different duplicate detections: one during uploads, and one
|
||||
that can be manually run afterwards to remove duplicates across albums.
|
||||
that can be manually run afterwards to remove duplicates and very similar files across albums.
|
||||
|
||||
## During uploads
|
||||
|
||||
@@ -16,7 +16,7 @@ When uploading, Ente will ignore exact duplicate files. This allows you to
|
||||
resume interrupted uploads, or drag and drop the same folder, or reinstall the
|
||||
app, and expect Ente to automatically skip duplicates and only add new files.
|
||||
|
||||
The duplicate detection works slightly different on each platform, to cater to
|
||||
The duplicate detection works slightly differently on each platform, to cater to
|
||||
the platform's nuances.
|
||||
|
||||
#### Mobile
|
||||
@@ -48,7 +48,7 @@ to album", and the actual files are not re-uploaded.
|
||||
|
||||
## Manual deduplication
|
||||
|
||||
Ente also provides a tool for manual de-duplication in _Settings → Backup →
|
||||
Ente provides a tool for manual de-duplication in _Settings → Backup → Free up space →
|
||||
Remove duplicates_. This is useful if you have an existing library with
|
||||
duplicates across different albums, but wish to keep only one copy.
|
||||
|
||||
@@ -57,6 +57,13 @@ single copy, and add symlinks to this copy within all existing albums. So your
|
||||
existing album structure remains unchanged, while the space consumed by the
|
||||
duplicate data is freed up.
|
||||
|
||||
## Filtering similar images
|
||||
|
||||
Ente also provides a tool for manual removal of images that are similar, but not the exact same, using our private ML. This feature can be found in _Settings → Backup → Free up space →
|
||||
Similar images_. This is useful if you've taken a lot of similar photos, potentiall even in different albums, and want to keep only the best ones.
|
||||
|
||||
During this filtering process you can choose which photos to keep and which to delete for each set of similar images. Ente will then automatically add symlinks for the kept photos to any albums that only had the deleted images. This way you can easily prune similar images, without worrying about accidentally removing the best ones from a certain album.
|
||||
|
||||
## Adding to Ente album creates symlinks
|
||||
|
||||
Note that once a file is in Ente, adding it to another Ente album will create a
|
||||
|
||||
@@ -8,6 +8,12 @@ description: Guide to configuring Ente CLI for Self Hosted Instance
|
||||
If you are self-hosting, you can configure Ente CLI to export data & perform
|
||||
basic administrative actions.
|
||||
|
||||
::: tip Installing Ente CLI
|
||||
|
||||
For instructions on installing the Ente CLI, see the [README available on Github](https://github.com/ente-io/ente/tree/main/cli/README.md).
|
||||
|
||||
:::
|
||||
|
||||
## Step 1: Configure endpoint
|
||||
|
||||
To do this, first configure the CLI to use your server's endpoint.
|
||||
|
||||
@@ -63,11 +63,20 @@ It has no relation to Backblaze, Wasabi or Scaleway.
|
||||
Each bucket's endpoint, region, key and secret should be configured accordingly
|
||||
if using an external bucket.
|
||||
|
||||
A sample configuration for `b2-eu-cen` is provided, which can be used for other
|
||||
2 buckets as well:
|
||||
If a bucket has SSL support enabled, set `s3.are_local_buckets` to `false`. Enable path-style URL by setting `s3.use_path_style_urls` to `true`.
|
||||
|
||||
::: note
|
||||
|
||||
You can configure this for individual buckets over defining top-level configuration if you are using the latest server image (August 2025)
|
||||
|
||||
:::
|
||||
|
||||
A sample configuration for `b2-eu-cen` is provided, which can be used for other 2 buckets as well:
|
||||
|
||||
```yaml
|
||||
b2-eu-cen:
|
||||
are_local_buckets: true
|
||||
use_path_style_urls: true
|
||||
key: <key>
|
||||
secret: <secret>
|
||||
endpoint: localhost:3200
|
||||
|
||||
@@ -89,7 +89,7 @@ cast.ente.yourdomain.tld {
|
||||
Reload Caddy for changes to take effect.
|
||||
|
||||
```shell
|
||||
sudo systemctl caddy reload
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
## Step 4: Verify the setup
|
||||
|
||||
@@ -22,8 +22,7 @@ can achieve this the following steps:
|
||||
# Change the DB name and DB user name if you use different
|
||||
# values.
|
||||
# If using Docker
|
||||
|
||||
docker exec -it <postgres-ente-container-name>
|
||||
docker exec -it <postgres-ente-container-name> sh
|
||||
psql -U pguser -d ente_db
|
||||
|
||||
# Or when using psql directly
|
||||
|
||||
@@ -96,8 +96,8 @@ provide correct credentials for proper connectivity within Museum.
|
||||
The `s3` section within `museum.yaml` is by default configured to use local
|
||||
MinIO buckets when using `quickstart.sh` or Docker Compose.
|
||||
|
||||
If you wish to use an external S3 provider, you can edit the configuration with
|
||||
your provider's credentials, and set `s3.are_local_buckets` to `false`.
|
||||
If you wish to use an external S3 provider with SSL, you can edit the configuration with
|
||||
your provider's credentials, and set `s3.are_local_buckets` to `false`. Additionally, you can configure this for specific buckets in the corresponding bucket sections in the Compose file.
|
||||
|
||||
If you are using default MinIO, it is accessible at port `3200`. Web Console can
|
||||
be accessed by enabling port `3201` in the Compose file.
|
||||
@@ -111,11 +111,11 @@ and [troubleshooting](/self-hosting/troubleshooting/uploads) sections.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------------------------------------- | -------------------------------------------- | ------- |
|
||||
| `s3.b2-eu-cen` | Primary hot storage S3 config | |
|
||||
| `s3.b2-eu-cen` | Primary hot storage bucket configuration | |
|
||||
| `s3.wasabi-eu-central-2-v3.compliance` | Whether to disable compliance lock on delete | `true` |
|
||||
| `s3.scw-eu-fr-v3` | Optional secondary S3 config | |
|
||||
| `s3.wasabi-eu-central-2-derived` | Derived data storage | |
|
||||
| `s3.are_local_buckets` | Use local MinIO-compatible storage | `false` |
|
||||
| `s3.scw-eu-fr-v3` | Cold storage bucket configuration | |
|
||||
| `s3.wasabi-eu-central-2-v3` | Secondary hot storage configuration | |
|
||||
| `s3.are_local_buckets` | | `true` |
|
||||
| `s3.use_path_style_urls` | Enable path-style URLs for MinIO | `false` |
|
||||
|
||||
### Encryption Keys
|
||||
@@ -171,6 +171,8 @@ smtp:
|
||||
email:
|
||||
# Optional name for sender
|
||||
sender-name:
|
||||
# Optional encryption
|
||||
encryption:
|
||||
```
|
||||
|
||||
| Variable | Description | Default |
|
||||
@@ -181,6 +183,7 @@ smtp:
|
||||
| `smtp.password` | SMTP auth password | |
|
||||
| `smtp.email` | Sender email address | |
|
||||
| `smtp.sender-name` | Custom name for email sender | |
|
||||
| `smtp.encryption` | Encryption method (tls, ssl) | |
|
||||
| `transmail.key` | Zeptomail API key | |
|
||||
|
||||
### WebAuthn Passkey Support
|
||||
|
||||
@@ -46,7 +46,7 @@ If running Museum without Docker, the code should be visible in the terminal
|
||||
# Change the DB name and DB user name if you use different
|
||||
# values.
|
||||
|
||||
# If using Docker docker exec -it <postgres-ente-container-name>
|
||||
# If using Docker docker exec -it <postgres-ente-container-name> sh
|
||||
psql -U pguser -d ente_db
|
||||
|
||||
# Or when using psql directly
|
||||
|
||||
167
infra/experiments/logs-viewer/README.md
Normal file
167
infra/experiments/logs-viewer/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Ente Log Viewer
|
||||
|
||||
A web-based log viewer for analyzing Ente application logs. This tool provides similar functionality to the mobile log viewer, allowing you to upload, filter, and analyze log files from customer support requests.
|
||||
|
||||
## Features
|
||||
|
||||
### 📁 File Upload
|
||||
- Drag and drop ZIP files containing log files
|
||||
- Automatic extraction and parsing of log files
|
||||
- Support for daily log files format (YYYY-M-D.log)
|
||||
|
||||
### 🔍 Search and Filtering
|
||||
- **Text Search**: Search through log messages, logger names, and error content
|
||||
- **Logger Filtering**: Use `logger:ServiceName` syntax to filter by specific loggers
|
||||
- **Wildcard Support**: Use `logger:Auth*` to match all loggers starting with "Auth"
|
||||
- **Level Filtering**: Filter by log levels (SEVERE, WARNING, INFO, etc.)
|
||||
- **Process Filtering**: Filter by foreground/background processes
|
||||
- **Timeline Filtering**: Filter by date/time ranges
|
||||
|
||||
### 📊 Analytics
|
||||
- Logger statistics showing most active components
|
||||
- Log level distribution charts
|
||||
- Click-to-filter from analytics charts
|
||||
|
||||
### 🎨 UI Features
|
||||
- **Color-coded log levels**: Red for SEVERE, orange for WARNING, etc.
|
||||
- **Process indicators**: Visual distinction between foreground and background processes
|
||||
- **Active filter chips**: Visual indication of applied filters with easy removal
|
||||
- **Log detail view**: Click any log entry for detailed information
|
||||
- **Sort options**: Sort by newest first or oldest first
|
||||
- **Responsive design**: Works on desktop and mobile devices
|
||||
|
||||
### 📤 Export
|
||||
- Export filtered logs as text files
|
||||
- Copy individual log entries to clipboard
|
||||
- Maintain original formatting and error details
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Application
|
||||
|
||||
1. **Local Development**:
|
||||
```bash
|
||||
cd infra/experiments/logs-viewer
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
Open http://localhost:8080 in your browser
|
||||
|
||||
2. **Upload Log Files**:
|
||||
- Drag and drop a ZIP file containing `.log` files
|
||||
- Or click "Choose ZIP File" to browse for files
|
||||
|
||||
### Log Format Support
|
||||
|
||||
The viewer understands the Ente log format as generated by `super_logging.dart`:
|
||||
|
||||
```
|
||||
[process] [loggerName] [LEVEL] [timestamp] message
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
- `[bg] [SyncService] [INFO] [2025-08-24 01:36:03.677678] Syncing started`
|
||||
- `[CollectionsService] [WARNING] [2025-08-24 01:36:04.123456] Connection failed`
|
||||
|
||||
**Multi-line Error Support**:
|
||||
- Automatically parses `⤷` error detail lines
|
||||
- Extracts stack traces and error IDs
|
||||
- Handles inline error messages and exceptions
|
||||
|
||||
### Filtering Examples
|
||||
|
||||
- **Search by text**: `connection failed`
|
||||
- **Filter by logger**: `logger:SyncService`
|
||||
- **Multiple loggers**: `logger:Sync* logger:Collection*`
|
||||
- **Combined search**: `logger:Auth* login failed`
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **Search**: Click search bar or start typing
|
||||
- **Clear search**: Click X button or clear the input
|
||||
- **Sort toggle**: Click sort arrow button
|
||||
- **Filter dialog**: Click filter button
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Supported Log Levels
|
||||
- **SHOUT**: Purple - Highest priority
|
||||
- **SEVERE**: Red - Errors and exceptions
|
||||
- **WARNING**: Orange - Warning conditions
|
||||
- **INFO**: Blue - Informational messages
|
||||
- **CONFIG**: Green - Configuration messages
|
||||
- **FINE/FINER/FINEST**: Gray - Debug messages
|
||||
|
||||
### Process Types
|
||||
- **Foreground**: Main app processes
|
||||
- **Background (bg)**: Background tasks
|
||||
- **Firebase Background (fbg)**: Firebase-related background processes
|
||||
|
||||
### Performance
|
||||
- Lazy loading: Only renders visible log entries
|
||||
- Efficient filtering: Client-side filtering with optimized algorithms
|
||||
- Memory management: Handles large log files (tested with 100k+ entries)
|
||||
|
||||
## Sample Log Files
|
||||
|
||||
For testing, you can use any ZIP file containing `.log` files from Ente mobile app logs.
|
||||
|
||||
## Development
|
||||
|
||||
### File Structure
|
||||
```
|
||||
logs-viewer/
|
||||
├── index.html # Main HTML structure
|
||||
├── styles.css # CSS styling
|
||||
├── script.js # JavaScript logic
|
||||
├── README.md # This documentation
|
||||
└── references/ # UI reference screenshots
|
||||
```
|
||||
|
||||
### Key JavaScript Classes
|
||||
- `LogViewer`: Main application class
|
||||
- Log parsing logic in `parseLogFile()` and `parseLogLine()`
|
||||
- Filter management in `applyCurrentFilter()`
|
||||
- UI rendering in `renderLogs()` and `createLogEntryHTML()`
|
||||
|
||||
### Adding Features
|
||||
1. **New Filter Types**: Extend `currentFilter` object and `matchesFilter()` method
|
||||
2. **New Log Formats**: Update parsing patterns in `parseLogLine()`
|
||||
3. **UI Components**: Add HTML elements and CSS classes, wire up in `initializeEventListeners()`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **ZIP file not loading**:
|
||||
- Ensure ZIP contains `.log` files
|
||||
- Check browser console for errors
|
||||
- Try a smaller file first
|
||||
|
||||
2. **Logs not parsing correctly**:
|
||||
- Check log format matches expected pattern
|
||||
- Look for console warnings about parsing failures
|
||||
- Verify timestamp format is correct
|
||||
|
||||
3. **Performance issues with large files**:
|
||||
- The viewer handles pagination (100 logs at a time)
|
||||
- Use filters to reduce the dataset
|
||||
- Close other browser tabs to free memory
|
||||
|
||||
4. **Search not working**:
|
||||
- Check for typos in logger names
|
||||
- Use wildcard syntax: `logger:Service*`
|
||||
- Search is case-insensitive for message content
|
||||
|
||||
### Browser Compatibility
|
||||
- **Recommended**: Chrome 80+, Firefox 75+, Safari 13+
|
||||
- **Required**: ES6 support, Fetch API, File API
|
||||
- **Dependencies**: JSZip library for ZIP file handling
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Real-time log streaming
|
||||
- Advanced regex search
|
||||
- Log correlation and grouping
|
||||
- Performance metrics dashboard
|
||||
- Dark theme support
|
||||
- Export to JSON/CSV formats
|
||||
110
infra/experiments/logs-viewer/ente-theme.css
Normal file
110
infra/experiments/logs-viewer/ente-theme.css
Normal file
@@ -0,0 +1,110 @@
|
||||
/* Ente Theme Variables based on web/packages/base/components/utils/theme.ts */
|
||||
:root {
|
||||
/* Light theme colors */
|
||||
--ente-color-accent-photos: #1db954;
|
||||
--ente-color-accent-auth: #9610d6;
|
||||
--ente-color-accent-locker: #5ba8ff;
|
||||
|
||||
/* Background colors */
|
||||
--ente-background-default: #fff;
|
||||
--ente-background-paper: #fff;
|
||||
--ente-background-paper2: #fbfbfb;
|
||||
--ente-background-search: #f3f3f3;
|
||||
|
||||
/* Text colors */
|
||||
--ente-text-base: #000;
|
||||
--ente-text-muted: rgba(0, 0, 0, 0.60);
|
||||
--ente-text-faint: rgba(0, 0, 0, 0.50);
|
||||
|
||||
/* Fill colors */
|
||||
--ente-fill-base: #000;
|
||||
--ente-fill-muted: rgba(0, 0, 0, 0.12);
|
||||
--ente-fill-faint: rgba(0, 0, 0, 0.04);
|
||||
--ente-fill-faint-hover: rgba(0, 0, 0, 0.08);
|
||||
--ente-fill-fainter: rgba(0, 0, 0, 0.02);
|
||||
|
||||
/* Stroke colors */
|
||||
--ente-stroke-base: #000;
|
||||
--ente-stroke-muted: rgba(0, 0, 0, 0.24);
|
||||
--ente-stroke-faint: rgba(0, 0, 0, 0.12);
|
||||
--ente-stroke-fainter: rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* Shadow */
|
||||
--ente-shadow-paper: 0px 0px 10px rgba(0, 0, 0, 0.25);
|
||||
--ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.16), 0px 3px 6px rgba(0, 0, 0, 0.12);
|
||||
--ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* Fixed colors */
|
||||
--ente-success: #1db954;
|
||||
--ente-warning: #ffc107;
|
||||
--ente-danger: #ea3f3f;
|
||||
--ente-danger-dark: #f53434;
|
||||
--ente-danger-light: #ff6565;
|
||||
|
||||
/* Secondary colors */
|
||||
--ente-secondary-main: #f5f5f5;
|
||||
--ente-secondary-hover: #e9e9e9;
|
||||
|
||||
/* Action colors */
|
||||
--ente-action-hover: rgba(0, 0, 0, 0.08);
|
||||
--ente-action-disabled: rgba(0, 0, 0, 0.50);
|
||||
|
||||
/* Typography */
|
||||
--ente-font-family: "Inter Variable", sans-serif;
|
||||
--ente-font-weight-regular: 500;
|
||||
--ente-font-weight-medium: 600;
|
||||
--ente-font-weight-bold: 700;
|
||||
|
||||
/* Border radius */
|
||||
--ente-border-radius: 8px;
|
||||
--ente-border-radius-small: 4px;
|
||||
|
||||
/* Spacing */
|
||||
--ente-spacing-xs: 4px;
|
||||
--ente-spacing-sm: 8px;
|
||||
--ente-spacing-md: 12px;
|
||||
--ente-spacing-lg: 16px;
|
||||
--ente-spacing-xl: 24px;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* Background colors */
|
||||
--ente-background-default: #000;
|
||||
--ente-background-paper: #1b1b1b;
|
||||
--ente-background-paper2: #252525;
|
||||
--ente-background-search: #1b1b1b;
|
||||
|
||||
/* Text colors */
|
||||
--ente-text-base: #fff;
|
||||
--ente-text-muted: rgba(255, 255, 255, 0.70);
|
||||
--ente-text-faint: rgba(255, 255, 255, 0.50);
|
||||
|
||||
/* Fill colors */
|
||||
--ente-fill-base: #fff;
|
||||
--ente-fill-muted: rgba(255, 255, 255, 0.16);
|
||||
--ente-fill-faint: rgba(255, 255, 255, 0.12);
|
||||
--ente-fill-faint-hover: rgba(255, 255, 255, 0.16);
|
||||
--ente-fill-fainter: rgba(255, 255, 255, 0.05);
|
||||
|
||||
/* Stroke colors */
|
||||
--ente-stroke-base: #fff;
|
||||
--ente-stroke-muted: rgba(255, 255, 255, 0.24);
|
||||
--ente-stroke-faint: rgba(255, 255, 255, 0.16);
|
||||
--ente-stroke-fainter: rgba(255, 255, 255, 0.12);
|
||||
|
||||
/* Shadow */
|
||||
--ente-shadow-paper: 0px 2px 12px rgba(0, 0, 0, 0.75);
|
||||
--ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.50), 0px 3px 6px rgba(0, 0, 0, 0.25);
|
||||
--ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.75);
|
||||
|
||||
/* Secondary colors */
|
||||
--ente-secondary-main: #2b2b2b;
|
||||
--ente-secondary-hover: #373737;
|
||||
|
||||
/* Action colors */
|
||||
--ente-action-hover: rgba(255, 255, 255, 0.16);
|
||||
--ente-action-disabled: rgba(255, 255, 255, 0.50);
|
||||
}
|
||||
}
|
||||
218
infra/experiments/logs-viewer/index.html
Normal file
218
infra/experiments/logs-viewer/index.html
Normal file
@@ -0,0 +1,218 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ente Log Viewer</title>
|
||||
|
||||
<!-- Material-UI CSS -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mui/material@latest/dist/index.css" />
|
||||
|
||||
<!-- Ente theme -->
|
||||
<link rel="stylesheet" href="ente-theme.css">
|
||||
|
||||
<!-- Custom styles -->
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<h1>📋 Ente Log Viewer</h1>
|
||||
<div class="header-actions">
|
||||
<button id="filter-btn" class="mui-icon-btn" title="Filter logs">
|
||||
<span class="material-icons">filter_list</span>
|
||||
<span class="filter-count" id="filter-count" style="display: none;"></span>
|
||||
</button>
|
||||
<button id="sort-btn" class="mui-icon-btn" title="Sort order">
|
||||
<span class="material-icons">arrow_downward</span>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<button class="mui-icon-btn dropdown-toggle" title="More actions">
|
||||
<span class="material-icons">more_vert</span>
|
||||
</button>
|
||||
<div class="dropdown-menu mui-menu">
|
||||
<button id="analytics-btn" class="dropdown-item mui-menu-item">
|
||||
<span class="material-icons">analytics</span>
|
||||
<span>Analytics</span>
|
||||
</button>
|
||||
<button id="export-btn" class="dropdown-item mui-menu-item">
|
||||
<span class="material-icons">download</span>
|
||||
<span>Export Logs</span>
|
||||
</button>
|
||||
<button id="clear-btn" class="dropdown-item mui-menu-item danger">
|
||||
<span class="material-icons">delete</span>
|
||||
<span>Clear Logs</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Upload Section -->
|
||||
<div class="upload-section" id="upload-section">
|
||||
<div class="upload-area" id="upload-area">
|
||||
<div class="upload-content">
|
||||
<div class="upload-icon">📁</div>
|
||||
<h2>Upload Log Files</h2>
|
||||
<p>Drag and drop a zip file containing log files, or click to browse</p>
|
||||
<input type="file" id="file-input" accept=".zip" hidden>
|
||||
<button id="browse-btn" class="primary-btn">Choose ZIP File</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content" id="main-content" style="display: none;">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-section">
|
||||
<div class="search-bar mui-search-container">
|
||||
<div class="mui-textfield">
|
||||
<input type="text" id="search-input" placeholder="Search logs... (try 'logger:SyncService' or text search)" class="mui-input" />
|
||||
<span class="mui-search-icon material-icons">search</span>
|
||||
<button id="clear-search" class="mui-clear-btn" style="display: none;">
|
||||
<span class="material-icons">clear</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Filter -->
|
||||
<div class="timeline-section" id="timeline-section" style="display: none;">
|
||||
<div class="timeline-header">
|
||||
<span class="material-icons">timeline</span>
|
||||
<span>Timeline Filter</span>
|
||||
<button id="timeline-toggle" class="mui-icon-btn timeline-btn">
|
||||
<span class="material-icons">timeline</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="timeline-controls" id="timeline-controls" style="display: none;">
|
||||
<div class="timeline-range">
|
||||
<div class="mui-textfield">
|
||||
<input type="datetime-local" id="start-time" class="mui-input" />
|
||||
</div>
|
||||
<span class="range-separator">to</span>
|
||||
<div class="mui-textfield">
|
||||
<input type="datetime-local" id="end-time" class="mui-input" />
|
||||
</div>
|
||||
<button id="reset-timeline" class="mui-button secondary">
|
||||
<span class="material-icons">refresh</span>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Filters -->
|
||||
<div class="active-filters" id="active-filters" style="display: none;">
|
||||
<div class="filter-chips" id="filter-chips"></div>
|
||||
</div>
|
||||
|
||||
<!-- Log Stats -->
|
||||
<div class="log-stats" id="log-stats">
|
||||
<span id="log-count">0 logs loaded</span>
|
||||
<span id="filtered-count"></span>
|
||||
</div>
|
||||
|
||||
<!-- Log List -->
|
||||
<div class="log-list-container">
|
||||
<div class="log-list" id="log-list">
|
||||
<!-- Log entries will be populated here -->
|
||||
</div>
|
||||
<div class="loading" id="loading" style="display: none;">Loading...</div>
|
||||
<div class="load-more" id="load-more" style="display: none;">
|
||||
<button class="secondary-btn">Load More</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Dialog -->
|
||||
<div class="dialog-overlay" id="filter-dialog" style="display: none;">
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h2>Filter Logs</h2>
|
||||
<button class="close-btn" id="close-filter">✕</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<!-- Log Levels -->
|
||||
<div class="filter-section">
|
||||
<h3>Log Levels</h3>
|
||||
<div class="level-chips" id="level-chips">
|
||||
<!-- Level chips will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process -->
|
||||
<div class="filter-section">
|
||||
<h3>Process</h3>
|
||||
<div class="process-list" id="process-list">
|
||||
<!-- Process checkboxes will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loggers -->
|
||||
<div class="filter-section">
|
||||
<h3>Loggers</h3>
|
||||
<div class="logger-list" id="logger-list">
|
||||
<!-- Logger checkboxes will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button id="clear-filters" class="secondary-btn">Clear All</button>
|
||||
<button id="cancel-filter" class="secondary-btn">Cancel</button>
|
||||
<button id="apply-filters" class="primary-btn">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Dialog -->
|
||||
<div class="dialog-overlay" id="analytics-dialog" style="display: none;">
|
||||
<div class="dialog">
|
||||
<div class="dialog-header">
|
||||
<h2>Logger Analytics</h2>
|
||||
<button class="close-btn" id="close-analytics">✕</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div id="analytics-content">
|
||||
<!-- Analytics charts will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button id="close-analytics-btn" class="secondary-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Detail Dialog -->
|
||||
<div class="dialog-overlay" id="detail-dialog" style="display: none;">
|
||||
<div class="dialog large">
|
||||
<div class="dialog-header">
|
||||
<h2>Log Details</h2>
|
||||
<button class="close-btn" id="close-detail">✕</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div id="detail-content">
|
||||
<!-- Log details will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button id="copy-log" class="secondary-btn">Copy</button>
|
||||
<button id="close-detail-btn" class="secondary-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Material-UI JavaScript -->
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@mui/material@latest/umd/material-ui.production.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1146
infra/experiments/logs-viewer/script.js
Normal file
1146
infra/experiments/logs-viewer/script.js
Normal file
File diff suppressed because it is too large
Load Diff
1065
infra/experiments/logs-viewer/styles.css
Normal file
1065
infra/experiments/logs-viewer/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
45
mobile/.gitignore
vendored
Normal file
45
mobile/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
melos_*.iml
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
72
mobile/analysis_options.yaml
Normal file
72
mobile/analysis_options.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
# For more linters, we can check https://dart-lang.github.io/linter/lints/index.html
|
||||
# or https://pub.dev/packages/lint (Effective dart)
|
||||
# use "flutter analyze ." or "dart analyze ." for running lint checks
|
||||
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
linter:
|
||||
rules:
|
||||
# Ref https://github.com/flutter/packages/blob/master/packages/flutter_lints/lib/flutter.yaml
|
||||
# Ref https://dart-lang.github.io/linter/lints/
|
||||
- avoid_print
|
||||
- avoid_unnecessary_containers
|
||||
- avoid_web_libraries_in_flutter
|
||||
- no_logic_in_create_state
|
||||
- prefer_const_constructors
|
||||
- prefer_const_constructors_in_immutables
|
||||
- prefer_const_declarations
|
||||
- prefer_const_literals_to_create_immutables
|
||||
- prefer_final_locals
|
||||
- require_trailing_commas
|
||||
- sized_box_for_whitespace
|
||||
- use_full_hex_values_for_flutter_colors
|
||||
- use_key_in_widget_constructors
|
||||
- cancel_subscriptions
|
||||
|
||||
|
||||
- avoid_empty_else
|
||||
- exhaustive_cases
|
||||
|
||||
# just style suggestions
|
||||
- sort_pub_dependencies
|
||||
- use_rethrow_when_possible
|
||||
- prefer_double_quotes
|
||||
- directives_ordering
|
||||
- always_use_package_imports
|
||||
- sort_child_properties_last
|
||||
- unawaited_futures
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
avoid_empty_else: error
|
||||
exhaustive_cases: error
|
||||
curly_braces_in_flow_control_structures: error
|
||||
directives_ordering: error
|
||||
require_trailing_commas: error
|
||||
always_use_package_imports: warning
|
||||
prefer_final_fields: error
|
||||
unused_import: error
|
||||
camel_case_types: error
|
||||
prefer_is_empty: warning
|
||||
use_rethrow_when_possible: info
|
||||
unused_field: warning
|
||||
use_key_in_widget_constructors: warning
|
||||
sort_child_properties_last: warning
|
||||
sort_pub_dependencies: warning
|
||||
library_private_types_in_public_api: warning
|
||||
constant_identifier_names: ignore
|
||||
prefer_const_constructors: warning
|
||||
prefer_const_declarations: warning
|
||||
prefer_const_constructors_in_immutables: warning
|
||||
prefer_final_locals: warning
|
||||
unnecessary_const: error
|
||||
cancel_subscriptions: error
|
||||
unrelated_type_equality_checks: error
|
||||
unnecessary_cast: info
|
||||
|
||||
|
||||
unawaited_futures: warning # convert to warning after fixing existing issues
|
||||
invalid_dependency: info
|
||||
use_build_context_synchronously: ignore # experimental lint, requires many changes
|
||||
prefer_interpolation_to_compose_strings: ignore # later too many warnings
|
||||
prefer_double_quotes: ignore # too many warnings
|
||||
avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides
|
||||
1
mobile/apps/auth/.gitignore
vendored
1
mobile/apps/auth/.gitignore
vendored
@@ -8,6 +8,7 @@
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
android/app/build/
|
||||
|
||||
# Editors
|
||||
.vscode/
|
||||
|
||||
@@ -34,6 +34,9 @@ android {
|
||||
ndkVersion flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
// Sets Java compatibility to Java 8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -56,7 +59,7 @@ android {
|
||||
applicationId "io.ente.auth"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion 21
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
@@ -115,4 +118,7 @@ flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {}
|
||||
dependencies {
|
||||
// For AGP 7.4+
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
6
mobile/apps/auth/android/app/proguard-rules.pro
vendored
Normal file
6
mobile/apps/auth/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
|
||||
-dontwarn com.google.errorprone.annotations.CheckReturnValue
|
||||
-dontwarn com.google.errorprone.annotations.Immutable
|
||||
-dontwarn com.google.errorprone.annotations.RestrictedApi
|
||||
@@ -6,6 +6,19 @@ allprojects {
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
|
||||
subprojects {
|
||||
afterEvaluate { project ->
|
||||
if (project.hasProperty('android')) {
|
||||
project.android {
|
||||
if (namespace == null) {
|
||||
namespace project.group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.3.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -382,6 +382,11 @@
|
||||
{
|
||||
"title": "CoinDCX"
|
||||
},
|
||||
{
|
||||
"title": "CoinTracking",
|
||||
"slug": "cointracking",
|
||||
"altNames": ["cointracking.info", "Coin Tracking"]
|
||||
},
|
||||
{
|
||||
"title": "colorado",
|
||||
"altNames": [
|
||||
@@ -735,6 +740,11 @@
|
||||
{
|
||||
"title": "Hivelocity"
|
||||
},
|
||||
{
|
||||
"title": "HRDocumentBox",
|
||||
"slug": "hrdocumentbox",
|
||||
"altNames": ["HRDocumentBox", "HR Document Box"]
|
||||
},
|
||||
{
|
||||
"title": "HSA Bank",
|
||||
"slug": "hsa_bank",
|
||||
@@ -1040,6 +1050,13 @@
|
||||
"MistralAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mobile01",
|
||||
"slug": "mobile01",
|
||||
"altNames": [
|
||||
"M01"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mozilla"
|
||||
},
|
||||
@@ -1219,6 +1236,12 @@
|
||||
"title": "Parqet",
|
||||
"slug": "parqet"
|
||||
},
|
||||
{
|
||||
"title": "Parallels",
|
||||
"slug": "parallels",
|
||||
"hex": "#E61E25",
|
||||
"altNames": ["Parallels Desktop", "Parallels VM"]
|
||||
},
|
||||
{
|
||||
"title": "Parsec"
|
||||
},
|
||||
@@ -1773,6 +1796,11 @@
|
||||
"uollet.com.br"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VHV",
|
||||
"slug": "vhv",
|
||||
"altNames": ["VHV", "VHV Versicherung"]
|
||||
},
|
||||
{
|
||||
"title": "Vikunja"
|
||||
},
|
||||
|
||||
27
mobile/apps/auth/assets/custom-icons/icons/cointracking.svg
Normal file
27
mobile/apps/auth/assets/custom-icons/icons/cointracking.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1220.8 227.9" style="enable-background:new 0 0 1220.8 227.9;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#0D253E;}
|
||||
.st1{fill:#008AFB;}
|
||||
</style>
|
||||
<title>CoinTracking light</title>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Layer_1-2">
|
||||
<path class="st0" d="M198.1,167c-30.2,0-54.7-24.5-54.7-54.7s24.5-54.7,54.7-54.7s54.7,24.5,54.7,54.7l0,0 C252.8,142.5,228.3,167,198.1,167z M198.1,81.6c-17,0-30.7,13.7-30.7,30.7s13.7,30.7,30.7,30.7s30.7-13.7,30.7-30.7l0,0 C228.8,95.4,215,81.6,198.1,81.6z"/>
|
||||
<path class="st0" d="M292.2,59.5h-23.6v107.9h23.6V59.5z"/>
|
||||
<path class="st0" d="M339.5,167.4h-23.8V59.5h23.8v16.2c6.2-12.5,21-18.5,33-18.5c26.1,0,41.1,16.9,41.1,47.3v62.8h-23.8v-60.1 c0-17.1-8.8-26.8-22.6-26.8c-14.1,0-27.7,7.6-27.7,28.9V167.4z"/>
|
||||
<path class="st1" d="M390.6,8.2h151.7v22.9h-65.9v136.3h-25V31.1h-60.9V8.2H390.6z"/>
|
||||
<path class="st1" d="M540.3,167.4h-23.8V59.5h23.8V79c7.4-13.5,15.4-19.5,29.6-21.8c7.5-1.2,15.7,1.8,19.2,4.2l-3.9,22 c-4.9-2.5-10.4-3.8-15.9-3.7c-20.3,0-28.9,20.3-28.9,49L540.3,167.4L540.3,167.4z"/>
|
||||
<path class="st1" d="M680.7,151.9c-7.2,11.8-22.9,17.8-36.3,17.8c-29.1,0-54.8-21.9-54.8-56.4s25.6-56.1,54.8-56.1 c12.9,0,28.9,5.3,36.3,17.5V59.5h23.6v107.9h-23.6V151.9z M647.2,146.6c17.6,0,33.3-12.2,33.3-33.5s-17.1-32.8-33.3-32.8 c-18,0-33,12.9-33,32.8S629.2,146.6,647.2,146.6L647.2,146.6z"/>
|
||||
<path class="st1" d="M778.4,57.2c17.1,0,32.6,6.7,42.7,18.7l-18.5,14.8c-5.8-6.7-14.8-10.4-24.3-10.4c-18,0-34,12.7-34,32.8 s15.9,33.7,34,33.7c9.5,0,18.5-3.9,24.3-10.6l18.7,14.6c-10.2,12-25.6,18.9-43,18.9c-31,0-57.5-22.4-57.5-56.6 S747.4,57.2,778.4,57.2z"/>
|
||||
<path class="st1" d="M860.3,117.2v50.1h-23.6V0.8h23.6V95l34.2-35.6h31.9l-46,46.9l56.4,61h-30.5L860.3,117.2z"/>
|
||||
<path class="st1" d="M967.4,59.5h-23.6v107.9h23.6V59.5z"/>
|
||||
<path class="st1" d="M1014.7,167.4h-23.8V59.5h23.8v16.2c6.2-12.5,21-18.5,33-18.5c26.1,0,41.1,16.9,41.1,47.3v62.8H1065v-60.1 c0-17.1-8.8-26.8-22.6-26.8c-14.1,0-27.7,7.6-27.7,28.9V167.4z"/>
|
||||
<path class="st1" d="M1220.2,111.5c0-12.3-4.5-24.2-12.5-33.6l13-17.1l-19.6-13l-11.9,15.7c-8.3-4.1-17.5-6.2-26.8-6.2 c-31.9,0-57.8,24.4-57.8,54.3s25.9,54.3,57.8,54.3c20.2,0,34.2,10.2,34.2,19.3s-14.1,19.3-34.2,19.3s-34.2-10.2-34.2-19.3h-23.6 c0,24,25.4,42.9,57.8,42.9s57.8-18.8,57.8-42.9c0-13.2-7.7-24.8-19.9-32.6C1212.5,142.5,1220.2,127.8,1220.2,111.5z M1128.2,111.5 c0-16.9,15.4-30.7,34.2-30.7s34.2,13.8,34.2,30.7s-15.4,30.7-34.2,30.7S1128.2,128.4,1128.2,111.5L1128.2,111.5z"/>
|
||||
<path class="st0" d="M81.4,170.5C36.4,170.5,0,134,0,89c0-21.6,8.6-42.3,23.9-57.6c31.8-31.8,83.4-31.8,115.2,0l0,0l-17.7,17.7 C99.3,27,63.6,27,41.5,49.1s-22.1,57.8,0,79.9s57.8,22.1,79.9,0l17.7,17.7C123.8,162,103.1,170.6,81.4,170.5z"/>
|
||||
<circle class="st0" cx="280.7" cy="15.5" r="15.5"/>
|
||||
<circle class="st1" cx="954.7" cy="15.5" r="15.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
26
mobile/apps/auth/assets/custom-icons/icons/mobile01.svg
Normal file
26
mobile/apps/auth/assets/custom-icons/icons/mobile01.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg width="320" height="280" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1">
|
||||
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<g id="Layer1000">
|
||||
<g id="Layer1002">
|
||||
<g id="Layer1003">
|
||||
<path d="m123.08,34.29c-66.43,0 -120.27,53.85 -120.27,120.27c0,66.43 53.85,120.27 120.27,120.27c66.47,0 120.32,-53.85 120.32,-120.27c0,-66.43 -53.85,-120.27 -120.32,-120.27zm0,215.67c-52.67,0 -95.36,-42.73 -95.36,-95.4c0,-52.67 42.68,-95.4 95.36,-95.4c52.72,0 95.4,42.73 95.4,95.4c0,52.67 -42.68,95.4 -95.4,95.4z" fill="#2a5e00" fill-rule="evenodd" id="path7"/>
|
||||
<g id="Layer1004">
|
||||
<g id="Layer1005">
|
||||
<path d="m138.72,146.29l59.61,-41.47l7.78,33.7l-67.39,7.78z" fill="#2a5e00" fill-rule="evenodd" id="path8"/>
|
||||
<path d="m110.88,146.29l-59.61,-41.47l-7.78,33.7l67.39,7.78z" fill="#2a5e00" fill-rule="evenodd" id="path9"/>
|
||||
</g>
|
||||
<path d="m43.95,192.02l74.62,49.75l87.12,-78.8l-161.75,29.05z" fill="#2a5e00" fill-rule="evenodd" id="path10"/>
|
||||
</g>
|
||||
<path d="m94.24,59.29l-30.48,-55.1l54.26,33.24l-23.79,21.86z" fill="#2a5e00" fill-rule="evenodd" id="path11"/>
|
||||
<path d="m202.64,78.1l30.43,-55.1l-54.22,33.24l23.79,21.86z" fill="#2a5e00" fill-rule="evenodd" id="path12"/>
|
||||
</g>
|
||||
<path d="m275.63,274.67l29.35,0l0,-240.76l-29.35,0l0,240.76z" fill="#2a5e00" fill-rule="evenodd" id="path13"/>
|
||||
<path d="m317.94,125.93c0,15.3 -12.33,27.63 -27.63,27.63c-15.26,0 -27.63,-12.33 -27.63,-27.63c0,-15.26 12.37,-27.63 27.63,-27.63c15.3,0 27.63,12.37 27.63,27.63z" fill="#2a5e00" fill-rule="evenodd" id="path14"/>
|
||||
<path d="m288.84,33.91l-41.76,0l16.76,45.99l23.58,0l1.42,-45.99z" fill="#2a5e00" fill-rule="evenodd" id="path15"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
4
mobile/apps/auth/assets/custom-icons/icons/parallels.svg
Normal file
4
mobile/apps/auth/assets/custom-icons/icons/parallels.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect x="20" y="10" width="10" height="80" rx="5" fill="#E61E25"/>
|
||||
<rect x="50" y="10" width="10" height="80" rx="5" fill="#E61E25"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 207 B |
27
mobile/apps/auth/assets/custom-icons/icons/vhv.svg
Normal file
27
mobile/apps/auth/assets/custom-icons/icons/vhv.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024" height="357" viewBox="-1.98252 -1.98252 201.02104 70.04904" id="svg3349">
|
||||
<defs id="defs3351"/>
|
||||
<path d="m 0,0 11.76,0 4.455,31.962 0.12,0 L 20.789,0 32.55,0 23.402,42.417 9.147,42.417 0,0" id="path3131" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 34.039,0 11.05,0 0,15.567 6.771,0 0,-15.567 11.05,0 0,42.417 -11.05,0 0,-17.465 -6.771,0 0,17.465 -11.05,0 0,-42.417" id="path3133" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 64.395,0 11.757,0 4.457,31.962 0.121,0 L 85.185,0 96.944,0 87.797,42.417 73.54,42.417 64.395,0" id="path3135" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 97.576,42.414 8.907,0 9.222,-42.41 -8.912,0 -9.217,42.41" id="path3137" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 111.026,42.424 8.908,-0.01 9.218,-42.41 -8.913,0 -9.213,42.42" id="path3139" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 124.564,42.414 8.906,0 9.217,-42.41 -8.906,0 -9.217,42.41" id="path3141" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 4.169,46.784 5.251,0 1.985,14.261 0.05,0 1.99,-14.261 5.247,0 -4.082,18.928 -6.363,0 -4.082,-18.928" id="path3143" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 19.356,46.784 11.216,0 0,4.031 -6.282,0 0,3.234 5.882,0 0,3.87 -5.882,0 0,3.763 6.517,0 0,4.03 -11.451,0 0,-18.928" id="path3145" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 54.93,52.247 0,-0.45 c 0,-1.117 -0.449,-2.043 -1.405,-2.043 -1.06,0 -1.538,0.823 -1.538,1.67 0,3.739 8.059,1.91 8.059,8.828 0,4.027 -2.357,5.832 -6.705,5.832 -4.082,0 -6.362,-1.404 -6.362,-5.331 l 0,-0.663 4.772,0 0,0.453 c 0,1.615 0.663,2.202 1.614,2.202 1.007,0 1.594,-0.798 1.594,-1.831 0,-3.739 -7.743,-1.88 -7.743,-8.586 0,-3.819 2.043,-5.913 6.205,-5.913 4.294,0 6.12,1.775 6.12,5.832 l -4.611,0" id="path3147" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 61.476,65.712 4.932,0 0,-18.928 -4.932,0 0,18.928 z" id="path3149" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 32.477,46.784 7.714,0 c 3.766,0 5.195,2.124 5.195,5.039 0,2.516 -0.979,4.16 -3.232,4.506 2.385,0.265 3.103,1.776 3.103,4.294 l 0,1.616 c 0,0.981 0,2.251 0.237,2.626 0.132,0.209 0.237,0.422 0.558,0.582 l 0,0.265 -5.25,0 C 40.325,64.705 40.325,62.9 40.325,62.109 l 0,-1.275 c 0,-2.147 -0.423,-2.704 -1.619,-2.704 l -1.296,0 0,7.582 -4.933,0 0,-18.928 z m 4.933,8.008 0.98,0 c 1.405,0 2.066,-0.9 2.066,-2.252 0,-1.541 -0.609,-2.202 -2.094,-2.202 l -0.952,0 0,4.454" id="path3151" style="fill:#1e1e1e;fill-opacity:1;fill-rule:evenodd;stroke:none"/>
|
||||
<path d="m 75.975,52.54 c 0,-2.147 -0.396,-2.786 -1.35,-2.786 -1.513,0 -1.67,1.381 -1.67,6.494 0,5.117 0.157,6.497 1.67,6.497 1.22,0 1.485,-1.063 1.485,-4.64 l 4.771,0 0,1.405 c 0,5.3 -3.101,6.574 -6.256,6.574 -5.54,0 -6.76,-2.784 -6.76,-9.836 0,-7.236 1.642,-9.833 6.76,-9.833 4.451,0 6.12,2.334 6.12,5.992 l 0,1.19 -4.77,0 0,-1.057" id="path3153" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 82.524,46.784 4.928,0 0,6.948 3.02,0 0,-6.948 4.932,0 0,18.928 -4.932,0 0,-7.793 -3.02,0 0,7.793 -4.928,0 0,-18.928" id="path3155" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 97.606,46.784 11.21,0 0,4.031 -6.282,0 0,3.234 5.886,0 0,3.87 -5.886,0 0,3.763 6.522,0 0,4.03 -11.45,0 0,-18.928" id="path3157" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 130.526,46.784 0,13.655 c 0,1.824 0.559,2.306 1.485,2.306 0.928,0 1.485,-0.482 1.485,-2.306 l 0,-13.655 4.929,0 0,12.408 c 0,5.298 -2.28,6.892 -6.414,6.892 -4.135,0 -6.412,-1.594 -6.412,-6.892 l 0,-12.408 4.927,0" id="path3159" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 140.415,46.784 5.51,0 3.185,11.665 0.05,0 0,-11.665 4.614,0 0,18.928 -5.407,0 -3.288,-11.689 -0.05,0 0,11.689 -4.613,0 0,-18.928" id="path3161" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 110.726,46.784 7.716,0 c 3.76,0 5.192,2.124 5.192,5.039 0,2.516 -0.979,4.16 -3.234,4.506 2.383,0.265 3.101,1.776 3.101,4.294 l 0,1.616 c 0,0.981 0,2.251 0.238,2.626 0.131,0.209 0.24,0.422 0.558,0.582 l 0,0.265 -5.251,0 c -0.477,-1.007 -0.477,-2.812 -0.477,-3.603 l 0,-1.275 c 0,-2.147 -0.422,-2.704 -1.612,-2.704 l -1.301,0 0,7.582 -4.93,0 0,-18.928 z m 4.93,8.008 0.979,0 c 1.404,0 2.07,-0.9 2.07,-2.252 0,-1.541 -0.61,-2.202 -2.095,-2.202 l -0.954,0 0,4.454" id="path3163" style="fill:#1e1e1e;fill-opacity:1;fill-rule:evenodd;stroke:none"/>
|
||||
<path d="m 162.334,55.428 6.361,0 0,10.284 -3.34,0 -0.113,-1.669 -0.05,0 c -0.662,1.617 -2.412,2.041 -4.082,2.041 -5.01,0 -5.459,-3.58 -5.459,-9.836 0,-6.336 1.22,-9.833 7.047,-9.833 3.502,0 5.993,1.775 5.993,6.36 l -4.771,0 c 0,-0.952 -0.07,-1.695 -0.266,-2.198 -0.185,-0.53 -0.554,-0.823 -1.139,-0.823 -1.612,0 -1.774,1.381 -1.774,6.494 0,5.117 0.161,6.497 1.67,6.497 1.034,0 1.639,-0.666 1.669,-3.978 l -1.75,0 0,-3.339" id="path3165" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 170.577,46.784 11.213,0 0,4.031 -6.28,0 0,3.234 5.885,0 0,3.87 -5.885,0 0,3.763 6.519,0 0,4.03 -11.452,0 0,-18.928" id="path3167" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
<path d="m 183.697,46.784 5.514,0 3.18,11.665 0.05,0 0,-11.665 4.615,0 0,18.928 -5.408,0 -3.284,-11.689 -0.05,0 0,11.689 -4.614,0 0,-18.928" id="path3169" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
|
||||
</svg>
|
||||
<!-- version: 20110311, original size: 197.056 66.084, border: 3% -->
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -6,14 +6,14 @@
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "debug",
|
||||
"program": "auth/lib/main.dart",
|
||||
"program": "mobile/apps/auth/lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://localhost:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Auth Android Dev",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "auth/lib/main.dart",
|
||||
"program": "mobile/apps/auth/lib/main.dart",
|
||||
"args": [
|
||||
"--dart-define",
|
||||
"endpoint=http://192.168.1.3:8080",
|
||||
@@ -25,21 +25,21 @@
|
||||
"name": "Auth iOS Dev",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "auth/lib/main.dart",
|
||||
"program": "mobile/apps/auth/lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.30:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Auth iOS Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "auth/lib/main.dart",
|
||||
"program": "mobile/apps/auth/lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart"]
|
||||
},
|
||||
{
|
||||
"name": "Auth Android Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "auth/lib/main.dart",
|
||||
"program": "mobile/apps/auth/lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart", "--flavor", "independent"]
|
||||
}
|
||||
]
|
||||
|
||||
Submodule mobile/apps/auth/flutter updated: edada7c56e...2663184aa7
@@ -3,7 +3,6 @@ PODS:
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- cupertino_http (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -40,6 +39,8 @@ PODS:
|
||||
- DKPhotoGallery/Resource (0.0.19):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- ente_qr (0.0.1):
|
||||
- Flutter
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
@@ -61,13 +62,12 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- Toast
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -87,9 +87,9 @@ PODS:
|
||||
- qr_code_scanner (0.2.0):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- SDWebImage (5.21.1):
|
||||
- SDWebImage/Core (= 5.21.1)
|
||||
- SDWebImage/Core (5.21.1)
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- Flutter
|
||||
@@ -102,37 +102,43 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- sodium_libs (2.2.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
- "sqlite3/common (= 3.46.1+1)"
|
||||
- "sqlite3/common (3.46.1+1)"
|
||||
- "sqlite3/dbstatvtab (3.46.1+1)":
|
||||
- sqlite3 (3.50.2):
|
||||
- sqlite3/common (= 3.50.2)
|
||||
- sqlite3/common (3.50.2)
|
||||
- sqlite3/dbstatvtab (3.50.2):
|
||||
- sqlite3/common
|
||||
- "sqlite3/fts5 (3.46.1+1)":
|
||||
- sqlite3/fts5 (3.50.2):
|
||||
- sqlite3/common
|
||||
- "sqlite3/perf-threadsafe (3.46.1+1)":
|
||||
- sqlite3/math (3.50.2):
|
||||
- sqlite3/common
|
||||
- "sqlite3/rtree (3.46.1+1)":
|
||||
- sqlite3/perf-threadsafe (3.50.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.50.2):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- "sqlite3 (~> 3.46.0+1)"
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.50.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.5)
|
||||
- Toast (4.1.1)
|
||||
- ua_client_hints (1.4.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- ente_qr (from `.symlinks/plugins/ente_qr/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- file_saver (from `.symlinks/plugins/file_saver/ios`)
|
||||
- fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
|
||||
@@ -155,8 +161,9 @@ DEPENDENCIES:
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -169,17 +176,18 @@ SPEC REPOS:
|
||||
- Sentry
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/darwin"
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
cupertino_http:
|
||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
ente_qr:
|
||||
:path: ".symlinks/plugins/ente_qr/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
@@ -224,51 +232,54 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sodium_libs:
|
||||
:path: ".symlinks/plugins/sodium_libs/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
ua_client_hints:
|
||||
:path: ".symlinks/plugins/ua_client_hints/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
ente_qr: f39434aa69ea0e71047b49316365b2737f8a20aa
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
|
||||
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
local_auth_darwin: fa4b06454df7df8e97c18d7ee55151c57e7af0de
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: 3e82a2daae39ba3b41ae6ee84a130494585460fc
|
||||
sqlite3_flutter_libs: 2c48c4ee7217fd653251975e43412250d5bcbbe2
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
|
||||
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
||||
PODFILE CHECKSUM: 78f002751f1a8f65042b8da97902ba4124271c5a
|
||||
|
||||
@@ -2,20 +2,21 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:ente_accounts/services/user_service.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/locale.dart';
|
||||
import "package:ente_auth/onboarding/view/onboarding_page.dart";
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/services/window_listener_service.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/app_update_dialog.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:ente_events/models/signed_in_event.dart';
|
||||
import 'package:ente_events/models/signed_out_event.dart';
|
||||
import 'package:ente_strings/l10n/strings_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -131,6 +132,7 @@ class _AppState extends State<App>
|
||||
localeListResolutionCallback: localResolutionCallBack,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
StringsLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
@@ -150,6 +152,7 @@ class _AppState extends State<App>
|
||||
localeListResolutionCallback: localResolutionCallBack,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
StringsLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
|
||||
@@ -1,95 +1,36 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/endpoint_updated_event.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/signed_out_event.dart';
|
||||
import 'package:ente_auth/models/key_attributes.dart';
|
||||
import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/directory_utils.dart';
|
||||
import 'package:ente_base/models/database.dart';
|
||||
import 'package:ente_configuration/base_configuration.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class Configuration {
|
||||
class Configuration extends BaseConfiguration {
|
||||
Configuration._privateConstructor();
|
||||
|
||||
static final Configuration instance = Configuration._privateConstructor();
|
||||
static const endpoint = String.fromEnvironment(
|
||||
"endpoint",
|
||||
defaultValue: kDefaultProductionEndpoint,
|
||||
);
|
||||
static const emailKey = "email";
|
||||
static const keyAttributesKey = "key_attributes";
|
||||
|
||||
static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
|
||||
static const keyKey = "key";
|
||||
static const secretKeyKey = "secret_key";
|
||||
static const authSecretKeyKey = "auth_secret_key";
|
||||
static const offlineAuthSecretKey = "offline_auth_secret_key";
|
||||
static const tokenKey = "token";
|
||||
static const encryptedTokenKey = "encrypted_token";
|
||||
static const userIDKey = "user_id";
|
||||
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
|
||||
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
|
||||
static const endPointKey = "endpoint";
|
||||
final List<String> onlineSecureKeys = [
|
||||
keyKey,
|
||||
secretKeyKey,
|
||||
authSecretKeyKey,
|
||||
];
|
||||
|
||||
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
|
||||
|
||||
static final _logger = Logger("Configuration");
|
||||
|
||||
String? _cachedToken;
|
||||
late SharedPreferences _preferences;
|
||||
String? _key;
|
||||
String? _secretKey;
|
||||
String? _authSecretKey;
|
||||
String? _offlineAuthKey;
|
||||
late FlutterSecureStorage _secureStorage;
|
||||
late String _tempDirectory;
|
||||
|
||||
String? _volatilePassword;
|
||||
|
||||
Future<void> init() async {
|
||||
@override
|
||||
Future<void> init(List<EnteBaseDatabase> dbs) async {
|
||||
await super.init(dbs);
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
sqfliteFfiInit();
|
||||
_secureStorage = const FlutterSecureStorage(
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock_this_device,
|
||||
),
|
||||
);
|
||||
_tempDirectory = (await DirectoryUtils.getDirectoryForInit()).path;
|
||||
final tempDirectory = io.Directory(_tempDirectory);
|
||||
try {
|
||||
final currentTime = DateTime.now().microsecondsSinceEpoch;
|
||||
if (tempDirectory.existsSync() &&
|
||||
(_preferences.getInt(lastTempFolderClearTimeKey) ?? 0) <
|
||||
(currentTime - kTempFolderDeletionTimeBuffer)) {
|
||||
await tempDirectory.delete(recursive: true);
|
||||
await _preferences.setInt(lastTempFolderClearTimeKey, currentTime);
|
||||
_logger.info("Cleared temp folder");
|
||||
} else {
|
||||
_logger.info("Skipping temp folder clear");
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.warning(e);
|
||||
}
|
||||
tempDirectory.createSync(recursive: true);
|
||||
await _initOnlineAccount();
|
||||
sqfliteFfiInit();
|
||||
await _initOfflineAccount();
|
||||
}
|
||||
|
||||
@@ -99,303 +40,10 @@ class Configuration {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initOnlineAccount() async {
|
||||
if (!_preferences.containsKey(tokenKey)) {
|
||||
for (final key in onlineSecureKeys) {
|
||||
unawaited(
|
||||
_secureStorage.delete(
|
||||
key: key,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_key = await _secureStorage.read(
|
||||
key: keyKey,
|
||||
);
|
||||
_secretKey = await _secureStorage.read(
|
||||
key: secretKeyKey,
|
||||
);
|
||||
_authSecretKey = await _secureStorage.read(
|
||||
key: authSecretKeyKey,
|
||||
);
|
||||
if (_key == null) {
|
||||
await logout(autoLogout: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout({bool autoLogout = false}) async {
|
||||
await _preferences.clear();
|
||||
for (String key in onlineSecureKeys) {
|
||||
await _secureStorage.delete(
|
||||
key: key,
|
||||
);
|
||||
}
|
||||
await AuthenticatorDB.instance.clearTable();
|
||||
_key = null;
|
||||
_cachedToken = null;
|
||||
_secretKey = null;
|
||||
_authSecretKey = null;
|
||||
Bus.instance.fire(SignedOutEvent());
|
||||
}
|
||||
|
||||
Future<KeyGenResult> generateKey(String password) async {
|
||||
// Create a master key
|
||||
final masterKey = CryptoUtil.generateKey();
|
||||
|
||||
// Create a recovery key
|
||||
final recoveryKey = CryptoUtil.generateKey();
|
||||
|
||||
// Encrypt master key and recovery key with each other
|
||||
final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
|
||||
final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, masterKey);
|
||||
|
||||
// Derive a key from the password that will be used to encrypt and
|
||||
// decrypt the master key
|
||||
final kekSalt = CryptoUtil.getSaltToDeriveKey();
|
||||
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
|
||||
utf8.encode(password),
|
||||
kekSalt,
|
||||
);
|
||||
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
|
||||
|
||||
// Encrypt the key with this derived key
|
||||
final encryptedKeyData =
|
||||
CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
|
||||
|
||||
// Generate a public-private keypair and encrypt the latter
|
||||
final keyPair = CryptoUtil.generateKeyPair();
|
||||
final encryptedSecretKeyData =
|
||||
CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey);
|
||||
|
||||
final attributes = KeyAttributes(
|
||||
CryptoUtil.bin2base64(kekSalt),
|
||||
CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
|
||||
CryptoUtil.bin2base64(encryptedKeyData.nonce!),
|
||||
CryptoUtil.bin2base64(keyPair.publicKey),
|
||||
CryptoUtil.bin2base64(encryptedSecretKeyData.encryptedData!),
|
||||
CryptoUtil.bin2base64(encryptedSecretKeyData.nonce!),
|
||||
derivedKeyResult.memLimit,
|
||||
derivedKeyResult.opsLimit,
|
||||
CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!),
|
||||
CryptoUtil.bin2base64(encryptedMasterKey.nonce!),
|
||||
CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!),
|
||||
CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!),
|
||||
);
|
||||
final privateAttributes = PrivateKeyAttributes(
|
||||
CryptoUtil.bin2base64(masterKey),
|
||||
CryptoUtil.bin2hex(recoveryKey),
|
||||
CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()),
|
||||
);
|
||||
return KeyGenResult(attributes, privateAttributes, loginKey);
|
||||
}
|
||||
|
||||
Future<Tuple2<KeyAttributes, Uint8List>> getAttributesForNewPassword(
|
||||
String password,
|
||||
) async {
|
||||
// Get master key
|
||||
final masterKey = getKey();
|
||||
|
||||
// Derive a key from the password that will be used to encrypt and
|
||||
// decrypt the master key
|
||||
final kekSalt = CryptoUtil.getSaltToDeriveKey();
|
||||
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
|
||||
utf8.encode(password),
|
||||
kekSalt,
|
||||
);
|
||||
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
|
||||
|
||||
// Encrypt the key with this derived key
|
||||
final encryptedKeyData =
|
||||
CryptoUtil.encryptSync(masterKey!, derivedKeyResult.key);
|
||||
|
||||
final existingAttributes = getKeyAttributes();
|
||||
|
||||
final updatedAttributes = existingAttributes!.copyWith(
|
||||
kekSalt: CryptoUtil.bin2base64(kekSalt),
|
||||
encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
|
||||
keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!),
|
||||
memLimit: derivedKeyResult.memLimit,
|
||||
opsLimit: derivedKeyResult.opsLimit,
|
||||
);
|
||||
return Tuple2(updatedAttributes, loginKey);
|
||||
}
|
||||
|
||||
// decryptSecretsAndGetLoginKey decrypts the master key and recovery key
|
||||
// with the given password and save them in local secure storage.
|
||||
// This method also returns the keyEncKey that can be used for performing
|
||||
// SRP setup for existing users.
|
||||
Future<Uint8List> decryptSecretsAndGetKeyEncKey(
|
||||
String password,
|
||||
KeyAttributes attributes, {
|
||||
Uint8List? keyEncryptionKey,
|
||||
}) async {
|
||||
_logger.info('Start decryptAndSaveSecrets');
|
||||
keyEncryptionKey ??= await CryptoUtil.deriveKey(
|
||||
utf8.encode(password),
|
||||
CryptoUtil.base642bin(attributes.kekSalt),
|
||||
attributes.memLimit,
|
||||
attributes.opsLimit,
|
||||
);
|
||||
|
||||
_logger.info('user-key done');
|
||||
Uint8List key;
|
||||
try {
|
||||
key = CryptoUtil.decryptSync(
|
||||
CryptoUtil.base642bin(attributes.encryptedKey),
|
||||
keyEncryptionKey,
|
||||
CryptoUtil.base642bin(attributes.keyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe('master-key failed, incorrect password?', e);
|
||||
throw Exception("Incorrect password");
|
||||
}
|
||||
_logger.info("master-key done");
|
||||
await setKey(CryptoUtil.bin2base64(key));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
CryptoUtil.base642bin(attributes.encryptedSecretKey),
|
||||
key,
|
||||
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
_logger.info("secret-key done");
|
||||
await setSecretKey(CryptoUtil.bin2base64(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
CryptoUtil.base642bin(getEncryptedToken()!),
|
||||
CryptoUtil.base642bin(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
_logger.info('appToken done');
|
||||
await setToken(
|
||||
CryptoUtil.bin2base64(token, urlSafe: true),
|
||||
);
|
||||
return keyEncryptionKey;
|
||||
}
|
||||
|
||||
Future<void> recover(String recoveryKey) async {
|
||||
// check if user has entered mnemonic code
|
||||
if (recoveryKey.contains(' ')) {
|
||||
final split = recoveryKey.split(' ');
|
||||
if (split.length != mnemonicKeyWordCount) {
|
||||
String wordThatIsFollowedByEmptySpaceInSplit = '';
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
String word = split[i];
|
||||
if (word.isEmpty) {
|
||||
wordThatIsFollowedByEmptySpaceInSplit =
|
||||
'\n\nExtra space after word at position $i';
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw AssertionError(
|
||||
'\nRecovery code should have $mnemonicKeyWordCount words, '
|
||||
'found ${split.length} words instead.$wordThatIsFollowedByEmptySpaceInSplit',
|
||||
);
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
final attributes = getKeyAttributes();
|
||||
Uint8List masterKey;
|
||||
try {
|
||||
masterKey = await CryptoUtil.decrypt(
|
||||
CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
|
||||
CryptoUtil.hex2bin(recoveryKey),
|
||||
CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
rethrow;
|
||||
}
|
||||
await setKey(CryptoUtil.bin2base64(masterKey));
|
||||
final secretKey = CryptoUtil.decryptSync(
|
||||
CryptoUtil.base642bin(attributes.encryptedSecretKey),
|
||||
masterKey,
|
||||
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
|
||||
);
|
||||
await setSecretKey(CryptoUtil.bin2base64(secretKey));
|
||||
final token = CryptoUtil.openSealSync(
|
||||
CryptoUtil.base642bin(getEncryptedToken()!),
|
||||
CryptoUtil.base642bin(attributes.publicKey),
|
||||
secretKey,
|
||||
);
|
||||
await setToken(
|
||||
CryptoUtil.bin2base64(token, urlSafe: true),
|
||||
);
|
||||
}
|
||||
|
||||
String getHttpEndpoint() {
|
||||
return _preferences.getString(endPointKey) ?? endpoint;
|
||||
}
|
||||
|
||||
Future<void> setHttpEndpoint(String endpoint) async {
|
||||
await _preferences.setString(endPointKey, endpoint);
|
||||
Bus.instance.fire(EndpointUpdatedEvent());
|
||||
}
|
||||
|
||||
String? getToken() {
|
||||
_cachedToken ??= _preferences.getString(tokenKey);
|
||||
return _cachedToken;
|
||||
}
|
||||
|
||||
bool isLoggedIn() {
|
||||
return getToken() != null;
|
||||
}
|
||||
|
||||
Future<void> setToken(String token) async {
|
||||
_cachedToken = token;
|
||||
await _preferences.setString(tokenKey, token);
|
||||
Bus.instance.fire(SignedInEvent());
|
||||
}
|
||||
|
||||
Future<void> setEncryptedToken(String encryptedToken) async {
|
||||
await _preferences.setString(encryptedTokenKey, encryptedToken);
|
||||
}
|
||||
|
||||
String? getEncryptedToken() {
|
||||
return _preferences.getString(encryptedTokenKey);
|
||||
}
|
||||
|
||||
String? getEmail() {
|
||||
return _preferences.getString(emailKey);
|
||||
}
|
||||
|
||||
Future<void> setEmail(String email) async {
|
||||
await _preferences.setString(emailKey, email);
|
||||
}
|
||||
|
||||
int? getUserID() {
|
||||
return _preferences.getInt(userIDKey);
|
||||
}
|
||||
|
||||
Future<void> setUserID(int userID) async {
|
||||
await _preferences.setInt(userIDKey, userID);
|
||||
}
|
||||
|
||||
Future<void> setKeyAttributes(KeyAttributes attributes) async {
|
||||
await _preferences.setString(keyAttributesKey, attributes.toJson());
|
||||
}
|
||||
|
||||
KeyAttributes? getKeyAttributes() {
|
||||
final jsonValue = _preferences.getString(keyAttributesKey);
|
||||
if (jsonValue == null) {
|
||||
return null;
|
||||
} else {
|
||||
return KeyAttributes.fromJson(jsonValue);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setKey(String key) async {
|
||||
_key = key;
|
||||
await _secureStorage.write(
|
||||
key: keyKey,
|
||||
value: key,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setSecretKey(String? secretKey) async {
|
||||
_secretKey = secretKey;
|
||||
await _secureStorage.write(
|
||||
key: secretKeyKey,
|
||||
value: secretKey,
|
||||
);
|
||||
await super.logout();
|
||||
}
|
||||
|
||||
Future<void> setAuthSecretKey(String? authSecretKey) async {
|
||||
@@ -406,14 +54,6 @@ class Configuration {
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List? getKey() {
|
||||
return _key == null ? null : CryptoUtil.base642bin(_key!);
|
||||
}
|
||||
|
||||
Uint8List? getSecretKey() {
|
||||
return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!);
|
||||
}
|
||||
|
||||
Uint8List? getAuthSecretKey() {
|
||||
return _authSecretKey == null
|
||||
? null
|
||||
@@ -426,24 +66,6 @@ class Configuration {
|
||||
: CryptoUtil.base642bin(_offlineAuthKey!);
|
||||
}
|
||||
|
||||
Uint8List getRecoveryKey() {
|
||||
final keyAttributes = getKeyAttributes()!;
|
||||
return CryptoUtil.decryptSync(
|
||||
CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
|
||||
getKey()!,
|
||||
CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
|
||||
);
|
||||
}
|
||||
|
||||
// Caution: This directory is cleared on app start
|
||||
String getTempDirectory() {
|
||||
return _tempDirectory;
|
||||
}
|
||||
|
||||
bool hasConfiguredAccount() {
|
||||
return getToken() != null && _key != null;
|
||||
}
|
||||
|
||||
bool hasOptedForOfflineMode() {
|
||||
return _preferences.getBool(hasOptedForOfflineModeKey) ?? false;
|
||||
}
|
||||
@@ -464,16 +86,4 @@ class Configuration {
|
||||
}
|
||||
await _preferences.setBool(hasOptedForOfflineModeKey, true);
|
||||
}
|
||||
|
||||
void setVolatilePassword(String volatilePassword) {
|
||||
_volatilePassword = volatilePassword;
|
||||
}
|
||||
|
||||
void resetVolatilePassword() {
|
||||
_volatilePassword = null;
|
||||
}
|
||||
|
||||
String? getVolatilePassword() {
|
||||
return _volatilePassword;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
import 'package:ente_events/models/event.dart';
|
||||
|
||||
class CodesUpdatedEvent extends Event {}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class EndpointUpdatedEvent extends Event {}
|
||||
@@ -1,3 +1,3 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
import 'package:ente_events/models/event.dart';
|
||||
|
||||
class IconsChangedEvent extends Event {}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
// NotificationEvent event is used to re-fresh the UI to show latest notification
|
||||
// (if any)
|
||||
class NotificationEvent extends Event {}
|
||||
@@ -1,3 +0,0 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class SignedInEvent extends Event {}
|
||||
@@ -1,3 +1 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
|
||||
class SignedOutEvent extends Event {}
|
||||
// TODO Implement this library.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import 'package:ente_auth/events/event.dart';
|
||||
import 'package:ente_events/models/event.dart';
|
||||
|
||||
class TriggerLogoutEvent extends Event {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
import 'package:ente_network/network.dart';
|
||||
|
||||
class AuthenticatorGateway {
|
||||
late Dio _enteDio;
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
"importAegisGuide": "Použijte možnost \"Export the vault\" v nastavení aplikace Aegis.",
|
||||
"import2FasGuide": "Použijte možnost \"Settings->Backup -Export\" v 2FA.\n\nPokud je Vaše záloha šifrovaná, budete muset zadat heslo pro její odemčení",
|
||||
"importLastpassGuide": "V nastavení aplikace Lastpass Authenticator vyberte možnost \"Transfer accounts\" a poté \"Export accounts to file\". Vygenerovaný soubor JSON následně nahrajte sem.",
|
||||
"importProtonAuthGuide": "K exportu kódů použijte možnost „Exportovat“ v nastavení aplikace Proton Authenticator.",
|
||||
"exportCodes": "Exportovat kódy",
|
||||
"importLabel": "Importovat",
|
||||
"importInstruction": "Vyberte, prosím, soubor obsahující seznam Vašich kódů v následujícím formátu",
|
||||
@@ -124,6 +125,7 @@
|
||||
"authToChangeYourEmail": "Pro změnu svého e-mailu se, prosím, ověřte",
|
||||
"authToChangeYourPassword": "Pro změnu svého hesla se, prosím, ověřte",
|
||||
"authToViewSecrets": "Pro zobrazení svých tajných údajů se musíte ověřit",
|
||||
"authToInitiateSignIn": "Proveďte ověření a přihlaste se k zálohování.",
|
||||
"ok": "Ok",
|
||||
"cancel": "Zrušit",
|
||||
"yes": "Ano",
|
||||
@@ -171,6 +173,7 @@
|
||||
"invalidQRCode": "Neplatný QR kód",
|
||||
"noRecoveryKeyTitle": "Nemáte obnovovací klíč?",
|
||||
"enterEmailHint": "Zadejte svou e-mailovou adresu",
|
||||
"enterNewEmailHint": "Zadejte svou novou e-mailovou adresu",
|
||||
"invalidEmailTitle": "Neplatná e-mailová adresa",
|
||||
"invalidEmailMessage": "Prosím, zadejte platnou e-mailovou adresu.",
|
||||
"deleteAccount": "Odstranit účet",
|
||||
@@ -509,6 +512,19 @@
|
||||
"supportEnte": "Podpořte <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Dejte nám hvězdu na Githubu",
|
||||
"free5GB": "5GB zdarma na <bold-green>ente</bold-green> Fotky",
|
||||
"loginWithAuthAccount": "Přihlaste se pomocí svého účtu Auth",
|
||||
"freeStorageOffer": "10% sleva na <bold-green>ente</bold-green> fotky",
|
||||
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok"
|
||||
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok",
|
||||
"advanced": "Pokročilé",
|
||||
"algorithm": "Algoritmus",
|
||||
"type": "Typ",
|
||||
"period": "Období",
|
||||
"digits": "Digitální",
|
||||
"importFromGallery": "Importovat z galerie",
|
||||
"errorCouldNotReadImage": "Nelze přečíst vybraný obrazový soubor.",
|
||||
"errorInvalidQRCode": "Neplatný QR kód",
|
||||
"errorInvalidQRCodeBody": "Naskenovaný QR kód není platným účtem 2FA.",
|
||||
"errorNoQRCode": "Nenalezen žádný QR kód",
|
||||
"errorGenericTitle": "Došlo k chybě",
|
||||
"errorGenericBody": "Při importu došlo k neočekávané chybě."
|
||||
}
|
||||
@@ -111,6 +111,7 @@
|
||||
"importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Einstellungen von Aegis.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.",
|
||||
"import2FasGuide": "Verwenden Sie unter \"Einstellungen → Backup\" die Option \"Exportieren\" in 2FAS.\n\nFalls Ihr Backup verschlüsselt ist, müssen Sie das Passwort eingeben, um das Backup zu entschlüsseln.",
|
||||
"importLastpassGuide": "Verwenden Sie die Option \"Konten übertragen → Konten in Datei exportieren\" in den Lastpass Authenticator Einstellungen. \nImportieren Sie anschließend die heruntergeladene JSON-Datei.",
|
||||
"importProtonAuthGuide": "Verwenden Sie die Option \"Exportieren\" in den Proton Authenticator Settings um Ihre Codes zu exportieren.",
|
||||
"exportCodes": "Codes exportieren",
|
||||
"importLabel": "Importieren",
|
||||
"importInstruction": "Bitte wählen Sie eine Datei die Codes in folgendem Format beinhaltet",
|
||||
@@ -519,5 +520,12 @@
|
||||
"algorithm": "Algorithmus",
|
||||
"type": "Typ",
|
||||
"period": "Periode",
|
||||
"digits": "Ziffern"
|
||||
"digits": "Ziffern",
|
||||
"importFromGallery": "Aus Galerie importieren",
|
||||
"errorCouldNotReadImage": "Die ausgewählte Bild-Datei konnte nicht verarbeitet werden.",
|
||||
"errorInvalidQRCode": "Ungültiger QR-Code",
|
||||
"errorInvalidQRCodeBody": "Der gescannte QR-Code ist kein gültiges 2FA-Konto.",
|
||||
"errorNoQRCode": "Kein QR-Code gefunden",
|
||||
"errorGenericTitle": "Ein Fehler ist aufgetreten",
|
||||
"errorGenericBody": "Beim Importieren ist ein unerwarteter Fehler aufgetreten."
|
||||
}
|
||||
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Χρήση κλειδιού ανάκτησης",
|
||||
"incorrectPasswordTitle": "Λάθος κωδικός πρόσβασης",
|
||||
"welcomeBack": "Καλωσορίσατε και πάλι!",
|
||||
"emailAlreadyRegistered": "Το email είναι ήδη καταχωρημένο.",
|
||||
"emailNotRegistered": "Το email δεν έχει καταχωρηθεί.",
|
||||
"madeWithLoveAtPrefix": "φτιαγμένη με ❤️ στο ",
|
||||
"supportDevs": "Εγγραφείτε στο <bold-green>ente</bold-green> για να μας υποστηρίξετε",
|
||||
"supportDiscount": "Χρησιμοποιήστε τον κωδικό κουπονιού \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
|
||||
@@ -171,6 +173,7 @@
|
||||
"invalidQRCode": "Μη έγκυρος κωδικός QR",
|
||||
"noRecoveryKeyTitle": "Χωρίς κλειδί ανάκτησης;",
|
||||
"enterEmailHint": "Εισάγετε τη διεύθυνση email σας",
|
||||
"enterNewEmailHint": "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας",
|
||||
"invalidEmailTitle": "Μη έγκυρη διεύθυνση email",
|
||||
"invalidEmailMessage": "Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση email.",
|
||||
"deleteAccount": "Διαγραφή λογαριασμού",
|
||||
@@ -258,6 +261,10 @@
|
||||
"areYouSureYouWantToLogout": "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;",
|
||||
"yesLogout": "Ναι, αποσύνδεση",
|
||||
"exit": "Εξοδος",
|
||||
"theme": "Θέμα",
|
||||
"lightTheme": "Φωτεινό",
|
||||
"darkTheme": "Σκοτεινό",
|
||||
"systemTheme": "Σύστημα",
|
||||
"verifyingRecoveryKey": "Επαλήθευση κλειδιού ανάκτησης...",
|
||||
"recoveryKeyVerified": "Το κλειδί ανάκτησης επαληθεύτηκε",
|
||||
"recoveryKeySuccessBody": "Τέλεια! Το κλειδί ανάκτησης σας είναι έγκυρο. Σας ευχαριστούμε για την επαλήθευση.\n\nΠαρακαλώ θυμηθείτε να κρατήσετε το κλειδί ανάκτησης σας και σε αντίγραφο ασφαλείας.",
|
||||
@@ -490,5 +497,24 @@
|
||||
"appLockNotEnabled": "Το κλείδωμα εφαρμογής δεν είναι ενεργοποιημένο",
|
||||
"appLockNotEnabledDescription": "Παρακαλώ ενεργοποιήστε το κλείδωμα εφαρμογής μέσω της επιλογής Ασφάλεια > Κλείδωμα εφαρμογής",
|
||||
"authToViewPasskey": "Παρακαλώ πιστοποιηθείτε για να δείτε το κλειδί πρόσβασης",
|
||||
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας."
|
||||
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας.",
|
||||
"duplicateCodes": "Διπλότυποι κωδικοί",
|
||||
"noDuplicates": "✨ Δεν υπάρχουν διπλότυπα",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Δεν υπάρχουν διπλότυπα αρχεία που μπορούν να εκκαθαριστούν",
|
||||
"deduplicateCodes": "Διπλότυποι κωδικοί",
|
||||
"deselectAll": "Αποεπιλογή όλων",
|
||||
"selectAll": "Επιλογή όλων",
|
||||
"deleteDuplicates": "Διαγραφή διπλότυπων",
|
||||
"plainHTML": "Απλό HTML",
|
||||
"dropReviewiOS": "Αφήστε μια κριτική στο App Store",
|
||||
"dropReviewAndroid": "Αφήστε μια κριτική στο Play Store",
|
||||
"giveUsAStarOnGithub": "Δώστε μας ένα αστέρι στο Github",
|
||||
"free5GB": "5GB δωρεάν στο <bold-green>ente</bold-green> Photos",
|
||||
"freeStorageOffer": "10% έκπτωση στο <bold-green>ente</bold-green> photos",
|
||||
"freeStorageOfferDescription": "Χρησιμοποιήστε τον κωδικό \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
|
||||
"advanced": "Για προχωρημένους",
|
||||
"algorithm": "Αλγόριθμος",
|
||||
"type": "Τύπος",
|
||||
"period": "Περίοδος",
|
||||
"digits": "Ψηφία"
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"setupFirstAccount": "Setup your first account",
|
||||
"importScanQrCode": "Scan a QR Code",
|
||||
"qrCode": "QR Code",
|
||||
"qr": "QR",
|
||||
"importEnterSetupKey": "Enter a setup key",
|
||||
"importAccountPageTitle": "Enter account details",
|
||||
"secretCanNotBeEmpty": "Secret can not be empty",
|
||||
@@ -111,6 +112,7 @@
|
||||
"importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.",
|
||||
"import2FasGuide": "Use the \"Settings->Backup -Export\" option in 2FAS.\n\nIf your backup is encrypted, you will need to enter the password to decrypt the backup",
|
||||
"importLastpassGuide": "Use the \"Transfer accounts\" option within Lastpass Authenticator Settings and press \"Export accounts to file\". Import the JSON downloaded.",
|
||||
"importProtonAuthGuide": "Use the \"Export\" option in Proton Authenticator Settings to export your codes.",
|
||||
"exportCodes": "Export codes",
|
||||
"importLabel": "Import",
|
||||
"importInstruction": "Please select a file that contains a list of your codes in the following format",
|
||||
@@ -138,6 +140,7 @@
|
||||
"existingUser": "Existing User",
|
||||
"newUser": "New to Ente",
|
||||
"delete": "Delete",
|
||||
"addTag": "Add tag",
|
||||
"enterYourPasswordHint": "Enter your password",
|
||||
"forgotPassword": "Forgot password",
|
||||
"oops": "Oops",
|
||||
@@ -519,5 +522,35 @@
|
||||
"algorithm": "Algorithm",
|
||||
"type": "Type",
|
||||
"period": "Period",
|
||||
"digits": "Digits"
|
||||
"digits": "Digits",
|
||||
"importFromGallery": "Import from gallery",
|
||||
"errorCouldNotReadImage": "Could not read the selected image file.",
|
||||
"errorInvalidQRCode": "Invalid QR Code",
|
||||
"errorInvalidQRCodeBody": "The scanned QR code is not a valid 2FA account.",
|
||||
"errorNoQRCode": "No QR code found",
|
||||
"errorGenericTitle": "An Error Occurred",
|
||||
"errorGenericBody": "An unexpected error occurred while importing.",
|
||||
"localBackupSettingsTitle": "Local backup",
|
||||
"localBackupSidebarTitle": "Local backup",
|
||||
"enableAutomaticBackups": "Enable automatic backups",
|
||||
"backupDescription": "This will automatically backup your data to an on-device location. Backups are updated whenever entries are added, edited or deleted",
|
||||
"currentLocation": "Current backup location:",
|
||||
"securityNotice": "Security notice",
|
||||
"backupSecurityNotice": "This encrypted backup holds your 2FA keys. If lost, you may not be able to recover your accounts. Keep it safe!",
|
||||
"locationUpdatedAndBackupCreated": "Location updated and initial backup created!",
|
||||
"initialBackupCreated": "Initial backup created!",
|
||||
"passwordTooShort": "Password must be at least 8 characters long.",
|
||||
"noDefaultBackupFolder": "Could not create default backup folder.",
|
||||
"backupLocationChoiceDescription": "Where do you want to save your backups?",
|
||||
"chooseBackupLocation": "Choose a backup location",
|
||||
"loadDefaultLocation": "Loading default location...",
|
||||
"couldNotDetermineLocation":"Could not determine location...",
|
||||
"saveAction":"Save",
|
||||
"saveBackup":"Save backup",
|
||||
"changeLocation": "Change location",
|
||||
"changeCurrentLocation": "Change current location",
|
||||
"done": "Done",
|
||||
"addNew": "Add new",
|
||||
"selected": "selected",
|
||||
"moveMultipleToTrashMessage": "Are you sure you want to move {count} item(s) to the trash?"
|
||||
}
|
||||
@@ -111,6 +111,7 @@
|
||||
"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.",
|
||||
"import2FasGuide": "Utilisez l'option \"Paramètres->Sauvegarde -Export\" dans 2FAS.\n\nSi votre sauvegarde est chiffrée, vous devrez entrer le mot de passe pour déchiffrer la sauvegarde",
|
||||
"importLastpassGuide": "Utilisez l'option \"Transférer des comptes\" dans les paramètres de l'authentificateur Lastpass et appuyez sur \"Exporter des comptes vers un fichier\". Importez le JSON téléchargé.",
|
||||
"importProtonAuthGuide": "Utilisez l'option \"Export\" dans les paramètres de Proton Authenticator pour exporter vos codes.",
|
||||
"exportCodes": "Exporter les codes",
|
||||
"importLabel": "Importer",
|
||||
"importInstruction": "Veuillez sélectionner un fichier qui contient une liste de vos codes dans le format suivant",
|
||||
@@ -519,5 +520,12 @@
|
||||
"algorithm": "Algorithme",
|
||||
"type": "Type",
|
||||
"period": "Période",
|
||||
"digits": "Chiffres"
|
||||
"digits": "Chiffres",
|
||||
"importFromGallery": "Importer depuis la galerie",
|
||||
"errorCouldNotReadImage": "Impossible de lire le fichier sélectionné.",
|
||||
"errorInvalidQRCode": "QR Code invalide",
|
||||
"errorInvalidQRCodeBody": "Le code QR scanné n'est pas un compte 2FA valide.",
|
||||
"errorNoQRCode": "Aucun code QR trouvé",
|
||||
"errorGenericTitle": "Une erreur s'est produite",
|
||||
"errorGenericBody": "Une erreur inattendue est survenue lors de l'importation."
|
||||
}
|
||||
@@ -111,6 +111,7 @@
|
||||
"importAegisGuide": "Naudokite „Aegis“ nustatymuose esančią parinktį Eksportuoti slėptuvę.\n\nJei jūsų saugykla užšifruota, turėsite įvesti saugyklos slaptažodį, kad iššifruotumėte saugyklą.",
|
||||
"import2FasGuide": "Naudokite programoje 2FAS esančią parinktį „Settings->2FAS Backup->Export to file“.\n\nJei atsarginė kopija užšifruota, turėsite įvesti slaptažodį, kad iššifruotumėte atsarginę kopiją.",
|
||||
"importLastpassGuide": "Naudokite „Lastpass Authenticator“ nustatymuose esančią parinktį „Transfer accounts“ (perkelti paskyras) ir paspauskite „Export accounts to file“ (eksportuoti paskyras į failą). Importuokite atsisiųstą JSON failą.",
|
||||
"importProtonAuthGuide": "Naudokite „Proton Authenticator“ nustatymuose esančią parinktį „Export“ (eksportuoti), kad eksportuotumėte savo kodus.",
|
||||
"exportCodes": "Eksportuoti kodus",
|
||||
"importLabel": "Importuoti",
|
||||
"importInstruction": "Pasirinkite failą, kuriame yra tokio formato jūsų kodų sąrašas",
|
||||
@@ -519,5 +520,12 @@
|
||||
"algorithm": "Algoritmas",
|
||||
"type": "Tipas",
|
||||
"period": "Laikotarpis",
|
||||
"digits": "Skaitmenys"
|
||||
"digits": "Skaitmenys",
|
||||
"importFromGallery": "Importuoti iš galerijos",
|
||||
"errorCouldNotReadImage": "Nepavyko perskaityti pasirinkto vaizdo failo.",
|
||||
"errorInvalidQRCode": "Netinkamas QR kodas",
|
||||
"errorInvalidQRCodeBody": "Nuskenuotas QR kodas nėra tinkama 2FA paskyra.",
|
||||
"errorNoQRCode": "QR kodas nerastas.",
|
||||
"errorGenericTitle": "Įvyko klaida",
|
||||
"errorGenericBody": "Importuojant įvyko netikėta klaida."
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
"timeBasedKeyType": "Oparte na czasie (TOTP)",
|
||||
"counterBasedKeyType": "Oparte na liczniku (HOTP)",
|
||||
"saveAction": "Zapisz",
|
||||
"nextTotpTitle": "następny",
|
||||
"nextTotpTitle": "dalej",
|
||||
"deleteCodeTitle": "Usunąć kod?",
|
||||
"deleteCodeMessage": "Czy na pewno chcesz usunąć ten kod? Ta akcja jest nieodwracalna.",
|
||||
"trashCode": "Przenieść kod do kosza?",
|
||||
@@ -111,6 +111,7 @@
|
||||
"importAegisGuide": "Użyj opcji \"Eksportuj sejf\" w ustawieniach Aegis.\n\nJeśli twój sejf jest zaszyfrowany, musisz wprowadzić hasło sejfu, aby odszyfrować sejf.",
|
||||
"import2FasGuide": "Użyj opcji \"Ustawienia->Kopia Zapasowa-Eksport\" w 2FAS.\n\nJeśli twoja kopia zapasowa jest zaszyfrowana, musisz wprowadzić hasło, aby odszyfrować kopię zapasową",
|
||||
"importLastpassGuide": "Użyj opcji \"Przenieś konta\" w Ustawieniach Lastpass Authenticator i naciśnij \"Eksportuj konta do pliku\". Zaimportuj pobrany plik JSON.",
|
||||
"importProtonAuthGuide": "Użyj opcji „Eksportuj” w ustawieniach Proton Authenticator, aby wyeksportować kody.",
|
||||
"exportCodes": "Eksportuj kody",
|
||||
"importLabel": "Importuj",
|
||||
"importInstruction": "Wybierz plik, który zawiera listę twoich kodów w następującym formacie",
|
||||
@@ -519,5 +520,12 @@
|
||||
"algorithm": "Algorytm",
|
||||
"type": "Rodzaj",
|
||||
"period": "Okres",
|
||||
"digits": "Cyfry"
|
||||
"digits": "Cyfry",
|
||||
"importFromGallery": "Importuj z galerii",
|
||||
"errorCouldNotReadImage": "Nie można odczytać wybranego pliku obrazu.",
|
||||
"errorInvalidQRCode": "Nieprawidłowy kod QR",
|
||||
"errorInvalidQRCodeBody": "Zeskanowany kod QR nie wskazuje na prawidłowe konto 2FA.",
|
||||
"errorNoQRCode": "Nie znaleziono kodu QR",
|
||||
"errorGenericTitle": "Wystąpił błąd",
|
||||
"errorGenericBody": "Podczas importowania wystąpił nieoczekiwany błąd."
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"incorrectDetails": "Felaktiga uppgifter",
|
||||
"pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen",
|
||||
"codeIssuerHint": "Utfärdare",
|
||||
"codeSecretKeyHint": "Secret Key",
|
||||
"codeSecretKeyHint": "Hemlig nyckel",
|
||||
"secret": "Säkerhetsnyckel",
|
||||
"all": "Alla",
|
||||
"notes": "Anteckningar",
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codeAccountHint": "Konto (du@domän.com)",
|
||||
"codeAccountHint": "Konto (du@domain.com)",
|
||||
"codeTagHint": "Tagg",
|
||||
"accountKeyType": "Typ av nyckel",
|
||||
"sessionExpired": "Sessionen har gått ut",
|
||||
@@ -68,7 +68,7 @@
|
||||
"reportABug": "Rapportera en bugg",
|
||||
"crashAndErrorReporting": "Krasch och felrapportering",
|
||||
"reportBug": "Rapportera bugg",
|
||||
"emailUsMessage": "Skicka e-mail till {email}",
|
||||
"emailUsMessage": "Skicka e-post till {email}",
|
||||
"@emailUsMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
@@ -79,7 +79,7 @@
|
||||
"contactSupport": "Kontakta support",
|
||||
"rateUsOnStore": "Betygsätt på {storeName}",
|
||||
"blog": "Blogg",
|
||||
"merchandise": "Merchandise",
|
||||
"merchandise": "Produkter",
|
||||
"verifyPassword": "Bekräfta lösenord",
|
||||
"pleaseWait": "Vänligen vänta...",
|
||||
"generatingEncryptionKeysTitle": "Skapar krypteringsnycklar...",
|
||||
@@ -104,13 +104,14 @@
|
||||
"importFromApp": "Importera koder från {appName}",
|
||||
"importGoogleAuthGuide": "Exportera dina konton från Google Authenticator till en QR-kod med alternativet \"Överföra konton\". Använd sedan en annan enhet och skanna QR-koden.\n\nTips: Du kan använda din bärbara dators webbkamera för att ta en bild av QR-koden.",
|
||||
"importSelectJsonFile": "Välj JSON-fil",
|
||||
"importSelectAppExport": "Välj {appName} exportfil",
|
||||
"importSelectAppExport": "Välj {appName} exporteringsfil",
|
||||
"importEnteEncGuide": "Välj den krypterade JSON-filen som exporteras från Ente",
|
||||
"importRaivoGuide": "Använd alternativet \"Exportera OTPs till zip-arkiv\" i Raivos inställningar.\n\nExtrahera zip-filen och importera JSON-filen.",
|
||||
"importBitwardenGuide": "Använd alternativet \"Exportera valv\" inom Bitwarden Tools och importera den okrypterade JSON-filen.",
|
||||
"importAegisGuide": "Använd alternativet \"Exportera valvet\" i Aegis inställningar.\n\nOm ditt valv är krypterat måste du ange valvlösenordet för att dekryptera valvet.",
|
||||
"import2FasGuide": "Använd alternativet \"Inställningar->Säkerhetskopiera -Exportera\" i 2FAS.\n\nOm din säkerhetskopia är krypterad måste du ange lösenordet för att dekryptera säkerhetskopian.",
|
||||
"importLastpassGuide": "Använd alternativet \"Överför konton\" i LastPass Authenticators inställningar och tryck på \"Exportera konton till fil\". Importera JSON-filen som laddas ner.",
|
||||
"importProtonAuthGuide": "Använd alternativet \"Exportera\" i Proton Authenticator-inställningarna för att exportera koder.",
|
||||
"exportCodes": "Exportera koder",
|
||||
"importLabel": "Importera",
|
||||
"importInstruction": "Vänligen välj en fil som innehåller en lista över dina koder i följande format",
|
||||
@@ -119,11 +120,11 @@
|
||||
"emailVerificationToggle": "E-postverifiering",
|
||||
"emailVerificationEnableWarning": "För att undvika att bli låst från ditt konto, se till att spara en kopia av din e-post 2FA utanför Ente Auth innan du aktiverar e-postverifiering.",
|
||||
"authToChangeEmailVerificationSetting": "Autentisera för att ändra din e-postadress",
|
||||
"authenticateGeneric": "Var god autentisera",
|
||||
"authenticateGeneric": "Vänligen autentisera",
|
||||
"authToViewYourRecoveryKey": "Autentisera för att visa din återställningsnyckel",
|
||||
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
|
||||
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
|
||||
"authToViewSecrets": "Autentisera för att visa din återställningsnyckel",
|
||||
"authToViewSecrets": "Vänligen autentisera för att visa din återställningsnyckel",
|
||||
"authToInitiateSignIn": "Vänligen autentisera för att initiera inloggning för säkerhetskopiering.",
|
||||
"ok": "OK",
|
||||
"cancel": "Avbryt",
|
||||
@@ -147,7 +148,7 @@
|
||||
"leaveFamily": "Lämna familjen",
|
||||
"leaveFamilyMessage": "Är du säker på att du vill lämna familjeplanen?",
|
||||
"inFamilyPlanMessage": "Du är på en familjeplan!",
|
||||
"hintForMobile": "Håll i på en kod för att redigera eller ta bort.",
|
||||
"hintForMobile": "Tryck länge på en kod för att redigera eller ta bort.",
|
||||
"hintForDesktop": "Högerklicka på en kod för att redigera eller ta bort.",
|
||||
"scan": "Skanna",
|
||||
"scanACode": "Skanna kod",
|
||||
@@ -191,7 +192,7 @@
|
||||
"oopsSomethingWentWrong": "Hoppsan! Något gick fel.",
|
||||
"selectLanguage": "Välj språk",
|
||||
"language": "Språk",
|
||||
"social": "Social",
|
||||
"social": "Socialt",
|
||||
"security": "Säkerhet",
|
||||
"lockscreen": "Låsskärm",
|
||||
"authToChangeLockscreenSetting": "Vänligen autentisera för att ändra låsskärms inställningar",
|
||||
@@ -200,7 +201,7 @@
|
||||
"authToViewYourActiveSessions": "Autentisera för att visa dina aktiva sessioner",
|
||||
"searchHint": "Sök...",
|
||||
"search": "Sök",
|
||||
"sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
|
||||
"sorryUnableToGenCode": "Tyvärr, kunde inte generera en kod för {issuerName}",
|
||||
"noResult": "Inga resultat",
|
||||
"addCode": "Lägg till kod",
|
||||
"scanAQrCode": "Skanna en QR-kod",
|
||||
@@ -215,7 +216,7 @@
|
||||
"error": "Fel",
|
||||
"recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
|
||||
"recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
|
||||
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ords nyckel på en säker plats.",
|
||||
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ordsnyckeln på en säker plats.",
|
||||
"doThisLater": "Gör detta senare",
|
||||
"saveKey": "Spara nyckel",
|
||||
"save": "Spara",
|
||||
@@ -254,7 +255,7 @@
|
||||
"insecureDevice": "Osäker enhet",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Tyvärr, kunde vi inte generera säkra nycklar på den här enheten.\n\nvänligen registrera dig från en annan enhet.",
|
||||
"howItWorks": "Så här fungerar det",
|
||||
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>end-to-end-krypterad</underline>.",
|
||||
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>totalsträckskrypterad</underline>.",
|
||||
"loginTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
|
||||
"logInLabel": "Logga in",
|
||||
"logout": "Logga ut",
|
||||
@@ -278,7 +279,7 @@
|
||||
"recoveryKeyVerifyReason": "Din återställningsnyckel är det enda sättet att återställa dina foton om du glömmer ditt lösenord. Du hittar din återställningsnyckel i Inställningar > Säkerhet.\n\nAnge din återställningsnyckel här för att verifiera att du har sparat den ordentligt.",
|
||||
"confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
|
||||
"confirm": "Bekräfta",
|
||||
"emailYourLogs": "Maila dina loggar",
|
||||
"emailYourLogs": "E-posta dina loggar",
|
||||
"pleaseSendTheLogsTo": "Vänligen skicka loggarna till \n{toEmail}",
|
||||
"copyEmailAddress": "Kopiera e-postadress",
|
||||
"exportLogs": "Exportera loggar",
|
||||
@@ -297,7 +298,7 @@
|
||||
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
|
||||
"updateAvailable": "Uppdatering tillgänglig",
|
||||
"update": "Uppdatera",
|
||||
"checking": "Kontrollerar ...",
|
||||
"checking": "Kontrollerar...",
|
||||
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
|
||||
"warning": "Varning",
|
||||
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
|
||||
@@ -306,7 +307,7 @@
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
},
|
||||
"authToExportCodes": "Autentisera för att exportera dina koder",
|
||||
"importSuccessTitle": "Jippi!",
|
||||
"importSuccessTitle": "Hurra!",
|
||||
"importSuccessDesc": "Du har importerat {count} koder!",
|
||||
"@importSuccessDesc": {
|
||||
"placeholders": {
|
||||
@@ -324,7 +325,7 @@
|
||||
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
|
||||
"tapToEnterCode": "Tryck för att ange kod",
|
||||
"resendEmail": "Skicka e-post igen",
|
||||
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
|
||||
"weHaveSendEmailTo": "Vi har skickat ett e-postmeddelande till <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
@@ -362,7 +363,7 @@
|
||||
"selectExportFormat": "Välj exportformat",
|
||||
"exportDialogDesc": "Krypterad export skyddas av ett lösenord som du väljer.",
|
||||
"encrypted": "Krypterad",
|
||||
"plainText": "Enkel text",
|
||||
"plainText": "Oformaterad text",
|
||||
"passwordToEncryptExport": "Lösenord för att kryptera export",
|
||||
"export": "Exportera",
|
||||
"useOffline": "Använd utan säkerhetskopior",
|
||||
@@ -374,14 +375,14 @@
|
||||
"compactMode": "Kompakt läge",
|
||||
"shouldHideCode": "Dölj koder",
|
||||
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
|
||||
"focusOnSearchBar": "Fokusera på sök vid appstart",
|
||||
"focusOnSearchBar": "Fokusera på sök vid uppstart av app",
|
||||
"confirmUpdatingkey": "Är du säker på att du vill uppdatera den hemliga nyckeln?",
|
||||
"minimizeAppOnCopy": "Minimera appen vid kopiering",
|
||||
"editCodeAuthMessage": "Autentisera för att redigera kod",
|
||||
"deleteCodeAuthMessage": "Autentisera för att radera kod",
|
||||
"showQRAuthMessage": "Autentisera för att visa QR-kod",
|
||||
"confirmAccountDeleteTitle": "Bekräfta radering av kontot",
|
||||
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente apps, om du använder någon.\n\nDina uppladdade data, över alla Ente appar, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
|
||||
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente applikationer, om du använder någon.\n\nDina uppladdade data, över alla Ente applikationer, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
|
||||
"androidBiometricHint": "Verifiera identitet",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -390,7 +391,7 @@
|
||||
"@androidBiometricNotRecognized": {
|
||||
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
"androidBiometricSuccess": "Slutförd",
|
||||
"androidBiometricSuccess": "Lyckades",
|
||||
"@androidBiometricSuccess": {
|
||||
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
|
||||
},
|
||||
@@ -441,7 +442,7 @@
|
||||
"signOutOtherDevices": "Logga ut andra enheter",
|
||||
"doNotSignOut": "Logga inte ut",
|
||||
"hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)",
|
||||
"hearUsExplanation": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!",
|
||||
"hearUsExplanation": "Vi spårar inte installationer. Det skulle hjälpa oss om du berättade hur du hittade oss!",
|
||||
"recoveryKeySaved": "Återställningsnyckel sparad i nedladdningsmappen!",
|
||||
"waitingForBrowserRequest": "Väntar på webbläsarbegäran...",
|
||||
"waitingForVerification": "Väntar på verifiering...",
|
||||
@@ -488,6 +489,8 @@
|
||||
"hideContent": "Dölj innehåll",
|
||||
"hideContentDescriptionAndroid": "Döljer appinnehåll i app-växlaren och inaktiverar skärmdumpar",
|
||||
"hideContentDescriptioniOS": "Döljer appinnehåll i app-växlaren",
|
||||
"autoLockFeatureDescription": "Tid efter vilken appen låses efter att ha satts i bakgrunden",
|
||||
"appLockDescription": "Välj mellan enhetens förvalda låsskärm och en anpassad låsskärm med en PIN-kod eller lösenord.",
|
||||
"pinLock": "Pinkodslås",
|
||||
"enterPin": "Ange PIN-kod",
|
||||
"setNewPin": "Ställ in ny PIN-kod",
|
||||
@@ -498,9 +501,31 @@
|
||||
"appLockOfflineModeWarning": "Du har valt att fortsätta utan säkerhetskopior. Om du glömmer ditt applås, kommer du att bli utelåst från att komma åt dina data.",
|
||||
"duplicateCodes": "Dubblettkoder",
|
||||
"noDuplicates": "✨ Inga dubbletter",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Du har inga dubbla koder som kan rensas",
|
||||
"deduplicateCodes": "Deduplicera koder",
|
||||
"deselectAll": "Avmarkera alla",
|
||||
"selectAll": "Markera alla",
|
||||
"deleteDuplicates": "Radera dubbletter",
|
||||
"plainHTML": "Ren HTML"
|
||||
"plainHTML": "Ren HTML",
|
||||
"tellUsWhatYouThink": "Berätta vad du tycker",
|
||||
"dropReviewiOS": "Skriv en recension på App Store",
|
||||
"dropReviewAndroid": "Skriv en recension på Play Store",
|
||||
"supportEnte": "Stöd <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Ge oss en stjärna på Github",
|
||||
"free5GB": "5 GB gratis på <bold-green>ente</bold-green> Foton",
|
||||
"loginWithAuthAccount": "Logga in med ditt Auth-konto",
|
||||
"freeStorageOffer": "10% rabatt på <bold-green>ente</bold-green> foton",
|
||||
"freeStorageOfferDescription": "Använd koden \"AUTH\" för att få 10% rabatt första året",
|
||||
"advanced": "Avancerad",
|
||||
"algorithm": "Algoritm",
|
||||
"type": "Typ",
|
||||
"period": "Tidsperiod",
|
||||
"digits": "Siffror",
|
||||
"importFromGallery": "Importera från galleri",
|
||||
"errorCouldNotReadImage": "Kunde inte läsa den valda bildfilen.",
|
||||
"errorInvalidQRCode": "Ogiltig QR-kod",
|
||||
"errorInvalidQRCodeBody": "Den skannade QR-koden är inte ett giltigt 2FA konto.",
|
||||
"errorNoQRCode": "Ingen QR-kod hittades",
|
||||
"errorGenericTitle": "Ett fel inträffade",
|
||||
"errorGenericBody": "Ett oväntat fel inträffade vid import."
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "妥善保管您的两步验证码",
|
||||
"onBoardingBody": "妥善保管您的双重认证代码",
|
||||
"onBoardingGetStarted": "开始",
|
||||
"setupFirstAccount": "设置您的第一个账户",
|
||||
"importScanQrCode": "扫描二维码",
|
||||
@@ -111,13 +111,14 @@
|
||||
"importAegisGuide": "使用 Aegis 设置中的“导出密码库”选项。\n\n如果您的密码库已加密,则需要输入密码库密码才能解密密码库。",
|
||||
"import2FasGuide": "使用 2FAS 中的“设置 -> 备份 -> 导出”选项。\n\n如果您的备份已加密,则需要输入密码来解密备份",
|
||||
"importLastpassGuide": "使用 Lastpass Authenticator 设置中的“转移账户”选项,然后按“将账户导出到文件”。导入下载的 JSON。",
|
||||
"importProtonAuthGuide": "使用 Proton Authenticator 设置中的“导出”选项导出您的代码。",
|
||||
"exportCodes": "导出代码",
|
||||
"importLabel": "导入",
|
||||
"importInstruction": "请选择一个包含以下格式的代码列表的文件",
|
||||
"importCodeDelimiterInfo": "代码可以用逗号或换行符分隔",
|
||||
"selectFile": "选择文件",
|
||||
"emailVerificationToggle": "电子邮件验证",
|
||||
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件两步验证的副本。",
|
||||
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件双重认证的副本。",
|
||||
"authToChangeEmailVerificationSetting": "请进行身份验证以更改电子邮件验证",
|
||||
"authenticateGeneric": "请验证",
|
||||
"authToViewYourRecoveryKey": "请验证以查看您的恢复密钥",
|
||||
@@ -155,7 +156,7 @@
|
||||
"verifyEmail": "验证电子邮件",
|
||||
"enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
|
||||
"lostDeviceTitle": "丢失了设备吗?",
|
||||
"twoFactorAuthTitle": "两步验证",
|
||||
"twoFactorAuthTitle": "双重认证",
|
||||
"passkeyAuthTitle": "通行密钥验证",
|
||||
"verifyPasskey": "验证通行密钥",
|
||||
"loginWithTOTP": "使用 TOTP 登录",
|
||||
@@ -519,5 +520,12 @@
|
||||
"algorithm": "算法",
|
||||
"type": "类型",
|
||||
"period": "周期",
|
||||
"digits": "数字"
|
||||
"digits": "数字",
|
||||
"importFromGallery": "从图库导入",
|
||||
"errorCouldNotReadImage": "无法读取所选图片文件。",
|
||||
"errorInvalidQRCode": "二维码无效",
|
||||
"errorInvalidQRCodeBody": "扫描的二维码不是有效的双重认证账户。",
|
||||
"errorNoQRCode": "未找到二维码",
|
||||
"errorGenericTitle": "出错了",
|
||||
"errorGenericBody": "导入时发生意外错误。"
|
||||
}
|
||||
@@ -6,4 +6,4 @@ export "package:ente_auth/l10n/arb/app_localizations.dart"
|
||||
|
||||
extension AppLocalizationsX on BuildContext {
|
||||
AppLocalizations get l10n => AppLocalizations.of(this);
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,38 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:ente_accounts/services/user_service.dart';
|
||||
import "package:ente_auth/app/view/app.dart";
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/locale.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/billing_service.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/services/window_listener_service.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/tools/app_lock.dart';
|
||||
import 'package:ente_auth/ui/tools/lock_screen.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/directory_utils.dart';
|
||||
import 'package:ente_auth/utils/lock_screen_settings.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/window_protocol_handler.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:ente_lock_screen/lock_screen_settings.dart';
|
||||
import 'package:ente_lock_screen/ui/app_lock.dart';
|
||||
import 'package:ente_lock_screen/ui/lock_screen.dart';
|
||||
import 'package:ente_logging/logging.dart';
|
||||
import 'package:ente_network/network.dart';
|
||||
import 'package:ente_strings/l10n/strings_localizations.dart';
|
||||
import 'package:ente_ui/theme/theme_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -89,7 +93,9 @@ void main() async {
|
||||
}
|
||||
|
||||
Future<void> _runInForeground() async {
|
||||
AppThemeConfig.initialize(EnteApp.auth);
|
||||
final savedThemeMode = _themeMode(await AdaptiveTheme.getThemeMode());
|
||||
final configuration = Configuration.instance;
|
||||
return await _runWithLogs(() async {
|
||||
_logger.info("Starting app in foreground");
|
||||
try {
|
||||
@@ -103,12 +109,19 @@ Future<void> _runInForeground() async {
|
||||
runApp(
|
||||
AppLock(
|
||||
builder: (args) => App(locale: locale),
|
||||
lockScreen: const LockScreen(),
|
||||
lockScreen: LockScreen(configuration),
|
||||
enabled: await LockScreenSettings.instance.shouldShowLockScreen(),
|
||||
locale: locale,
|
||||
lightTheme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
savedThemeMode: savedThemeMode,
|
||||
localeListResolutionCallback: localResolutionCallBack,
|
||||
localizationsDelegates: const [
|
||||
...StringsLocalizations.localizationsDelegates,
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
],
|
||||
supportedLocales: appSupportedLocales,
|
||||
backgroundLockLatency: const Duration(seconds: 0),
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -154,13 +167,13 @@ Future<void> _init(bool bool, {String? via}) async {
|
||||
await PreferenceService.instance.init();
|
||||
await CodeStore.instance.init();
|
||||
await CodeDisplayStore.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await Network.instance.init();
|
||||
await UserService.instance.init();
|
||||
await Configuration.instance.init([AuthenticatorDB.instance]);
|
||||
await Network.instance.init(Configuration.instance);
|
||||
await UserService.instance.init(Configuration.instance, const HomePage());
|
||||
await AuthenticatorService.instance.init();
|
||||
await BillingService.instance.init();
|
||||
await NotificationService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
await IconUtils.instance.init();
|
||||
await LockScreenSettings.instance.init();
|
||||
await LockScreenSettings.instance.init(Configuration.instance);
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
const freeProductID = "free";
|
||||
const stripe = "stripe";
|
||||
const appStore = "appstore";
|
||||
const playStore = "playstore";
|
||||
|
||||
class Subscription {
|
||||
final String productID;
|
||||
final int storage;
|
||||
final String originalTransactionID;
|
||||
final String paymentProvider;
|
||||
final int expiryTime;
|
||||
final String price;
|
||||
final String period;
|
||||
final Attributes? attributes;
|
||||
|
||||
Subscription({
|
||||
required this.productID,
|
||||
required this.storage,
|
||||
required this.originalTransactionID,
|
||||
required this.paymentProvider,
|
||||
required this.expiryTime,
|
||||
required this.price,
|
||||
required this.period,
|
||||
this.attributes,
|
||||
});
|
||||
|
||||
bool isValid() {
|
||||
return expiryTime > DateTime.now().microsecondsSinceEpoch;
|
||||
}
|
||||
|
||||
bool isYearlyPlan() {
|
||||
return 'year' == period;
|
||||
}
|
||||
|
||||
static fromMap(Map<String, dynamic>? map) {
|
||||
if (map == null) return null;
|
||||
return Subscription(
|
||||
productID: map['productID'],
|
||||
storage: map['storage'],
|
||||
originalTransactionID: map['originalTransactionID'],
|
||||
paymentProvider: map['paymentProvider'],
|
||||
expiryTime: map['expiryTime'],
|
||||
price: map['price'],
|
||||
period: map['period'],
|
||||
attributes: map["attributes"] != null
|
||||
? Attributes.fromJson(map["attributes"])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Attributes {
|
||||
bool? isCancelled;
|
||||
String? customerID;
|
||||
|
||||
Attributes({
|
||||
this.isCancelled,
|
||||
this.customerID,
|
||||
});
|
||||
|
||||
Attributes.fromJson(dynamic json) {
|
||||
isCancelled = json["isCancelled"];
|
||||
customerID = json["customerID"];
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
|
||||
class UserDetails {
|
||||
final String email;
|
||||
final int usage;
|
||||
final int fileCount;
|
||||
final int sharedCollectionsCount;
|
||||
final Subscription subscription;
|
||||
final FamilyData? familyData;
|
||||
final ProfileData? profileData;
|
||||
|
||||
UserDetails(
|
||||
this.email,
|
||||
this.usage,
|
||||
this.fileCount,
|
||||
this.sharedCollectionsCount,
|
||||
this.subscription,
|
||||
this.familyData,
|
||||
this.profileData,
|
||||
);
|
||||
|
||||
bool isPartOfFamily() {
|
||||
return familyData?.members?.isNotEmpty ?? false;
|
||||
}
|
||||
|
||||
bool isFamilyAdmin() {
|
||||
assert(isPartOfFamily(), "verify user is part of family before calling");
|
||||
final FamilyMember currentUserMember = familyData!.members!
|
||||
.firstWhere((element) => element.email.trim() == email.trim());
|
||||
return currentUserMember.isAdmin;
|
||||
}
|
||||
|
||||
// getFamilyOrPersonalUsage will return total usage for family if user
|
||||
// belong to family group. Otherwise, it will return storage consumed by
|
||||
// current user
|
||||
int getFamilyOrPersonalUsage() {
|
||||
return isPartOfFamily() ? familyData!.getTotalUsage() : usage;
|
||||
}
|
||||
|
||||
int getFreeStorage() {
|
||||
return max(
|
||||
isPartOfFamily()
|
||||
? (familyData!.storage - familyData!.getTotalUsage())
|
||||
: (subscription.storage - (usage)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
int getTotalStorage() {
|
||||
return isPartOfFamily() ? familyData!.storage : subscription.storage;
|
||||
}
|
||||
|
||||
factory UserDetails.fromMap(Map<String, dynamic> map) {
|
||||
return UserDetails(
|
||||
map['email'] as String,
|
||||
map['usage'] as int,
|
||||
(map['fileCount'] ?? 0) as int,
|
||||
(map['sharedCollectionsCount'] ?? 0) as int,
|
||||
Subscription.fromMap(map['subscription']),
|
||||
FamilyData.fromMap(map['familyData']),
|
||||
ProfileData.fromJson(map['profileData']),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FamilyMember {
|
||||
final String email;
|
||||
final int usage;
|
||||
final String id;
|
||||
final bool isAdmin;
|
||||
|
||||
FamilyMember(this.email, this.usage, this.id, this.isAdmin);
|
||||
|
||||
factory FamilyMember.fromMap(Map<String, dynamic> map) {
|
||||
return FamilyMember(
|
||||
(map['email'] ?? '') as String,
|
||||
map['usage'] as int,
|
||||
map['id'] as String,
|
||||
map['isAdmin'] as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
class ProfileData {
|
||||
bool canDisableEmailMFA;
|
||||
bool isEmailMFAEnabled;
|
||||
bool isTwoFactorEnabled;
|
||||
|
||||
// Constructor with default values
|
||||
ProfileData({
|
||||
this.canDisableEmailMFA = false,
|
||||
this.isEmailMFAEnabled = false,
|
||||
this.isTwoFactorEnabled = false,
|
||||
});
|
||||
|
||||
// Factory method to create ProfileData instance from JSON
|
||||
factory ProfileData.fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) null;
|
||||
|
||||
return ProfileData(
|
||||
canDisableEmailMFA: json!['canDisableEmailMFA'] ?? false,
|
||||
isEmailMFAEnabled: json['isEmailMFAEnabled'] ?? false,
|
||||
isTwoFactorEnabled: json['isTwoFactorEnabled'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert ProfileData instance to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'canDisableEmailMFA': canDisableEmailMFA,
|
||||
'isEmailMFAEnabled': isEmailMFAEnabled,
|
||||
'isTwoFactorEnabled': isTwoFactorEnabled,
|
||||
};
|
||||
}
|
||||
String toJsonString() => json.encode(toJson());
|
||||
}
|
||||
class FamilyData {
|
||||
final List<FamilyMember>? members;
|
||||
|
||||
// Storage available based on the family plan
|
||||
final int storage;
|
||||
final int expiryTime;
|
||||
|
||||
FamilyData(this.members, this.storage, this.expiryTime);
|
||||
|
||||
int getTotalUsage() {
|
||||
return members!.map((e) => e.usage).toList().sum;
|
||||
}
|
||||
|
||||
static fromMap(Map<String, dynamic>? map) {
|
||||
if (map == null) return null;
|
||||
assert(map['members'] != null && map['members'].length >= 0);
|
||||
final members = List<FamilyMember>.from(
|
||||
map['members'].map((x) => FamilyMember.fromMap(x)),
|
||||
);
|
||||
return FamilyData(
|
||||
members,
|
||||
map['storage'] as int,
|
||||
map['expiryTime'] as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_accounts/pages/email_entry_page.dart';
|
||||
import 'package:ente_accounts/pages/login_page.dart';
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_accounts/pages/password_reentry_page.dart';
|
||||
import 'package:ente_auth/app/view/app.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/locale.dart';
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
import 'package:ente_auth/ui/account/email_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/logout_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
@@ -24,6 +23,7 @@ import 'package:ente_auth/ui/settings/language_picker.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
@@ -260,17 +260,22 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
void _navigateToSignUpPage() {
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const EmailEntryPage();
|
||||
page = EmailEntryPage(Configuration.instance);
|
||||
} else {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
page = PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.set,
|
||||
const HomePage(),
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
page = const PasswordReentryPage();
|
||||
page = PasswordReentryPage(
|
||||
Configuration.instance,
|
||||
const HomePage(),
|
||||
);
|
||||
} else {
|
||||
// All is well, user just has not subscribed
|
||||
page = const HomePage();
|
||||
@@ -288,17 +293,22 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
void _navigateToSignInPage() {
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const LoginPage();
|
||||
page = LoginPage(Configuration.instance);
|
||||
} else {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
page = PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.set,
|
||||
const HomePage(),
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
page = const PasswordReentryPage();
|
||||
page = PasswordReentryPage(
|
||||
Configuration.instance,
|
||||
const HomePage(),
|
||||
);
|
||||
} else {
|
||||
// All is well, user just has not subscribed
|
||||
// page = getSubscriptionPage(isOnBoarding: true);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/all_icon_data.dart';
|
||||
@@ -23,6 +22,7 @@ import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
|
||||
@@ -54,9 +54,14 @@ class ViewQrPage extends StatelessWidget {
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
code?.account ?? '',
|
||||
style: enteTextTheme.largeBold,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text(
|
||||
code?.account ?? '',
|
||||
style: enteTextTheme.largeBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -73,9 +78,14 @@ class ViewQrPage extends StatelessWidget {
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
code?.issuer ?? '',
|
||||
style: enteTextTheme.largeBold,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Text(
|
||||
code?.issuer ?? '',
|
||||
style: enteTextTheme.largeBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||
import 'package:ente_auth/gateway/authenticator.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
@@ -17,6 +15,8 @@ import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:ente_events/models/signed_in_event.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_accounts/ente_accounts.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_auth/models/subscription.dart';
|
||||
import 'package:ente_auth/models/billing_plan.dart';
|
||||
import 'package:ente_network/network.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
|
||||
|
||||
155
mobile/apps/auth/lib/services/local_backup_service.dart
Normal file
155
mobile/apps/auth/lib/services/local_backup_service.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:ente_auth/models/export/ente.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:intl/intl.dart'; //for time based file naming
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class LocalBackupService {
|
||||
final _logger = Logger('LocalBackupService');
|
||||
static final LocalBackupService instance =
|
||||
LocalBackupService._privateConstructor();
|
||||
LocalBackupService._privateConstructor();
|
||||
|
||||
static const int _maxBackups = 2;
|
||||
|
||||
// to create an encrypted backup file if the toggle is on
|
||||
Future<void> triggerAutomaticBackup() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final isEnabled = prefs.getBool('isAutoBackupEnabled') ?? false;
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final backupPath = prefs.getString('autoBackupPath');
|
||||
if (backupPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = FlutterSecureStorage();
|
||||
final password = await storage.read(key: 'autoBackupPassword');
|
||||
if (password == null || password.isEmpty) {
|
||||
_logger.warning("Automatic backup skipped: password not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.info("Change detected, triggering automatic encrypted backup...");
|
||||
|
||||
final plainTextContent = await CodeStore.instance.getCodesForExport();
|
||||
|
||||
if (plainTextContent.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final kekSalt = CryptoUtil.getSaltToDeriveKey();
|
||||
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
|
||||
utf8.encode(password),
|
||||
kekSalt,
|
||||
);
|
||||
|
||||
final encResult = await CryptoUtil.encryptData(
|
||||
utf8.encode(plainTextContent),
|
||||
derivedKeyResult.key,
|
||||
);
|
||||
|
||||
final encContent = CryptoUtil.bin2base64(encResult.encryptedData!);
|
||||
final encNonce = CryptoUtil.bin2base64(encResult.header!);
|
||||
|
||||
final EnteAuthExport data = EnteAuthExport(
|
||||
version: 1,
|
||||
encryptedData: encContent,
|
||||
encryptionNonce: encNonce,
|
||||
kdfParams: KDFParams(
|
||||
memLimit: derivedKeyResult.memLimit,
|
||||
opsLimit: derivedKeyResult.opsLimit,
|
||||
salt: CryptoUtil.bin2base64(kekSalt),
|
||||
),
|
||||
);
|
||||
|
||||
final encryptedJson = jsonEncode(data.toJson());
|
||||
|
||||
final now = DateTime.now();
|
||||
final formatter = DateFormat('yyyy-MM-dd_HH-mm-ss');
|
||||
final formattedDate = formatter.format(now);
|
||||
final fileName = 'ente-auth-auto-backup-$formattedDate.json';
|
||||
|
||||
final filePath = '$backupPath/$fileName';
|
||||
final backupFile = File(filePath);
|
||||
|
||||
await backupFile.writeAsString(encryptedJson);
|
||||
await _manageOldBackups(backupPath);
|
||||
|
||||
_logger.info('Automatic encrypted backup successful! Saved to: $filePath');
|
||||
} catch (e, s) {
|
||||
_logger.severe('Silent error during automatic backup', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _manageOldBackups(String backupPath) async {
|
||||
try {
|
||||
_logger.info("Checking for old backups to clean up...");
|
||||
final directory = Directory(backupPath);
|
||||
|
||||
// fetch all filenames in the folder, filter out ente backup files
|
||||
final files = directory.listSync()
|
||||
.where((entity) =>
|
||||
entity is File &&
|
||||
entity.path.split('/').last.startsWith('ente-auth-auto-backup-'),)
|
||||
.map((entity) => entity as File)
|
||||
.toList();
|
||||
|
||||
// sort the fetched files in asc order (oldest first because the name is a timestamp)
|
||||
files.sort((a, b) => a.path.compareTo(b.path));
|
||||
|
||||
// if we have more files than our limit, delete the oldest ones (current limit=_maxBackups)
|
||||
while (files.length > _maxBackups) {
|
||||
// remove the oldest file (at index 0) from the list
|
||||
final fileToDelete = files.removeAt(0);
|
||||
// and delete it from the device's storage..
|
||||
await fileToDelete.delete();
|
||||
_logger.info('Deleted old backup: ${fileToDelete.path}');
|
||||
}
|
||||
_logger.info('Backup count is now ${files.length}. Cleanup complete.');
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error during old backup cleanup', e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAllBackupsIn(String path) async {
|
||||
try {
|
||||
_logger.info("Deleting all backups in old location: $path");
|
||||
final directory = Directory(path);
|
||||
|
||||
if (!await directory.exists()) {
|
||||
_logger.warning("Old backup directory not found. Nothing to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
final files = directory.listSync()
|
||||
.where((entity) =>
|
||||
entity is File &&
|
||||
entity.path.split('/').last.startsWith('ente-auth-auto-backup-'),)
|
||||
.map((entity) => entity as File)
|
||||
.toList();
|
||||
|
||||
if (files.isEmpty) {
|
||||
_logger.info("No old backup files found to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (final file in files) {
|
||||
await file.delete();
|
||||
_logger.info('Deleted: ${file.path}');
|
||||
}
|
||||
_logger.info("Successfully cleaned up old backup location.");
|
||||
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error during full backup cleanup of old directory', e, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/icons_changed_event.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
enum CodeSortKey {
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/services/notification_service.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_network/network.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -4,13 +4,14 @@ import 'dart:io';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:ente_auth/utils/directory_utils.dart';
|
||||
import 'package:ente_base/ente_base.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
class AuthenticatorDB {
|
||||
class AuthenticatorDB extends EnteBaseDatabase {
|
||||
static const _databaseName = "ente.authenticator.db";
|
||||
static const _databaseVersion = 1;
|
||||
|
||||
|
||||
@@ -14,6 +14,31 @@ class CodeDisplayStore {
|
||||
|
||||
late CodeStore _codeStore;
|
||||
|
||||
final ValueNotifier<bool> isSelectionModeActive = ValueNotifier(false);
|
||||
final ValueNotifier<Set<String>> selectedCodeIds = ValueNotifier(<String>{});
|
||||
|
||||
// toggles the selection status of a code
|
||||
void toggleSelection(String codeId){
|
||||
final newSelection = Set<String>.from(selectedCodeIds.value);
|
||||
|
||||
if(newSelection.contains(codeId)){
|
||||
newSelection.remove(codeId);
|
||||
}
|
||||
|
||||
else{
|
||||
newSelection.add(codeId);
|
||||
}
|
||||
|
||||
selectedCodeIds.value = newSelection; //if we selected atleast one code, then we're in selection mode.. else: exit selection mode
|
||||
isSelectionModeActive.value = newSelection.isNotEmpty;
|
||||
}
|
||||
|
||||
//method to clear the entire selection
|
||||
void clearSelection(){
|
||||
selectedCodeIds.value = <String>{};
|
||||
isSelectionModeActive.value = false;
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
_codeStore = CodeStore.instance;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/local_backup_service.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class CodeStore {
|
||||
@@ -64,6 +65,27 @@ class CodeStore {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> updateCode(Code originalCode, Code updatedCode, {bool shouldSync = true}) async {
|
||||
if (updatedCode.generatedID == null) return;
|
||||
|
||||
await _authenticatorService.updateEntry(
|
||||
updatedCode.generatedID!,
|
||||
updatedCode.toOTPAuthUrlFormat(),
|
||||
shouldSync,
|
||||
_authenticatorService.getAccountMode(),
|
||||
);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
|
||||
final bool isMajorChange = originalCode.issuer != updatedCode.issuer ||
|
||||
originalCode.account != updatedCode.account ||
|
||||
originalCode.secret != updatedCode.secret ||
|
||||
originalCode.display.note != updatedCode.display.note;
|
||||
|
||||
if (isMajorChange) {
|
||||
LocalBackupService.instance.triggerAutomaticBackup().ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Code>> getAllCodes({
|
||||
AccountMode? accountMode,
|
||||
bool sortCodes = true,
|
||||
@@ -95,7 +117,6 @@ class CodeStore {
|
||||
}
|
||||
|
||||
if (sortCodes) {
|
||||
// sort codes by issuer,account
|
||||
codes.sort((firstCode, secondCode) {
|
||||
if (secondCode.isPinned && !firstCode.isPinned) return 1;
|
||||
if (!secondCode.isPinned && firstCode.isPinned) return -1;
|
||||
@@ -121,12 +142,15 @@ class CodeStore {
|
||||
AccountMode? accountMode,
|
||||
List<Code>? existingAllCodes,
|
||||
}) async {
|
||||
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final allCodes = existingAllCodes ?? (await getAllCodes(accountMode: mode));
|
||||
bool isExistingCode = false;
|
||||
bool hasSameCode = false;
|
||||
|
||||
for (final existingCode in allCodes) {
|
||||
if (existingCode.hasError) continue;
|
||||
|
||||
if (code.generatedID != null &&
|
||||
existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
@@ -155,6 +179,7 @@ class CodeStore {
|
||||
shouldSync,
|
||||
mode,
|
||||
);
|
||||
LocalBackupService.instance.triggerAutomaticBackup().ignore();
|
||||
}
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
return result;
|
||||
@@ -164,6 +189,7 @@ class CodeStore {
|
||||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
await _authenticatorService.deleteEntry(code.generatedID!, mode);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
LocalBackupService.instance.triggerAutomaticBackup().ignore();
|
||||
}
|
||||
|
||||
bool _isOfflineImportRunning = false;
|
||||
@@ -214,7 +240,6 @@ class CodeStore {
|
||||
'importingCode: genID ${eachCode.generatedID} & isAlreadyPresent $alreadyPresent',
|
||||
);
|
||||
if (!alreadyPresent) {
|
||||
// Avoid conflict with generatedID of online codes
|
||||
eachCode.generatedID = null;
|
||||
final AddResult result = await CodeStore.instance.addCode(
|
||||
eachCode,
|
||||
@@ -236,10 +261,21 @@ class CodeStore {
|
||||
_isOfflineImportRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getCodesForExport() async {
|
||||
final allCodes = await getAllCodes(sortCodes: false);
|
||||
String data = "";
|
||||
for (final code in allCodes) {
|
||||
if (code.hasError) continue;
|
||||
data += "${code.toOTPAuthUrlFormat()}\n";
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum AddResult {
|
||||
newCode,
|
||||
duplicate,
|
||||
updateCode,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
|
||||
@@ -9,12 +9,11 @@ import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/onboarding/view/view_qr_page.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/code_timer_progress.dart';
|
||||
import 'package:ente_auth/ui/components/bottom_action_bar_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/share/code_share.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
@@ -22,6 +21,7 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:ente_lock_screen/local_authentication_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
@@ -103,7 +103,6 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ignorePin = widget.sortKey != null && widget.sortKey == CodeSortKey.manual;
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
if (isMaskingEnabled != PreferenceService.instance.shouldHideCodes()) {
|
||||
isMaskingEnabled = PreferenceService.instance.shouldHideCodes();
|
||||
_hideCode = isMaskingEnabled;
|
||||
@@ -118,96 +117,110 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
}
|
||||
final l10n = context.l10n;
|
||||
|
||||
Widget getCardContents(AppLocalizations l10n) {
|
||||
return Stack(
|
||||
Widget getCardContents(AppLocalizations l10n, {required bool isSelected}) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
return Stack(
|
||||
children: [
|
||||
if (!ignorePin && widget.code.isPinned)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.pinnedBgColor,
|
||||
),
|
||||
size: widget.isCompactMode
|
||||
? const Size(24, 24)
|
||||
: const Size(39, 39),
|
||||
),
|
||||
),
|
||||
if (widget.code.isTrashed && kDebugMode)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.warning700,
|
||||
),
|
||||
size: const Size(39, 39),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (!ignorePin && widget.code.isPinned)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.pinnedBgColor,
|
||||
),
|
||||
size: widget.isCompactMode
|
||||
? const Size(24, 24)
|
||||
: const Size(39, 39),
|
||||
),
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgress(
|
||||
key: ValueKey('period_${widget.code.period}'),
|
||||
period: widget.code.period,
|
||||
isCompactMode: widget.isCompactMode,
|
||||
timeOffsetInMilliseconds:
|
||||
PreferenceService.instance.timeOffsetInMilliSeconds(),
|
||||
),
|
||||
if (widget.code.isTrashed && kDebugMode)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: CustomPaint(
|
||||
painter: PinBgPainter(
|
||||
color: colorScheme.warning700,
|
||||
),
|
||||
size: const Size(39, 39),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
: const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.code.type.isTOTPCompatible)
|
||||
CodeTimerProgress(
|
||||
key: ValueKey('period_${widget.code.period}'),
|
||||
period: widget.code.period,
|
||||
isCompactMode: widget.isCompactMode,
|
||||
timeOffsetInMilliseconds:
|
||||
PreferenceService.instance.timeOffsetInMilliSeconds(),
|
||||
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_getTopRow(isSelected: isSelected),
|
||||
widget.isCompactMode
|
||||
? const SizedBox.shrink()
|
||||
: const SizedBox(height: 4),
|
||||
_getBottomRow(l10n),
|
||||
],
|
||||
),
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
: const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
_shouldShowLargeIcon ? _getIcon() : const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
_getTopRow(),
|
||||
widget.isCompactMode
|
||||
? const SizedBox.shrink()
|
||||
: const SizedBox(height: 4),
|
||||
_getBottomRow(l10n),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
: const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
if (!ignorePin && widget.code.isPinned) ...[
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: widget.isCompactMode
|
||||
? const EdgeInsets.only(right: 4, top: 4)
|
||||
: const EdgeInsets.only(right: 6, top: 6),
|
||||
child: SvgPicture.asset(
|
||||
"assets/svg/pin-card.svg",
|
||||
width: widget.isCompactMode ? 8 : null,
|
||||
height: widget.isCompactMode ? 8 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
: const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
if (!ignorePin && widget.code.isPinned) ...[
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: widget.isCompactMode
|
||||
? const EdgeInsets.only(right: 4, top: 4)
|
||||
: const EdgeInsets.only(right: 6, top: 6),
|
||||
child: SvgPicture.asset(
|
||||
"assets/svg/pin-card.svg",
|
||||
width: widget.isCompactMode ? 8 : null,
|
||||
height: widget.isCompactMode ? 8 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget clippedCard(AppLocalizations l10n) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
|
||||
return ValueListenableBuilder<Set<String>>(
|
||||
valueListenable: CodeDisplayStore.instance.selectedCodeIds,
|
||||
builder: (context, selectedIds, child) {
|
||||
final isSelected = selectedIds.contains(widget.code.secret);
|
||||
|
||||
Widget clippedCard(AppLocalizations l10n) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
color: isSelected
|
||||
? colorScheme.primary400.withValues(alpha: 0.08)
|
||||
: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
//add purple overlay when selected
|
||||
border: isSelected
|
||||
? Border.all(color: colorScheme.primary400, width: 2)
|
||||
: null,
|
||||
boxShadow:
|
||||
widget.code.isPinned ? colorScheme.pinnedCardBoxShadow : [],
|
||||
(widget.code.isPinned && !isSelected) ? colorScheme.pinnedCardBoxShadow : [],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
@@ -215,7 +228,12 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
final store = CodeDisplayStore.instance;
|
||||
if (store.isSelectionModeActive.value) {
|
||||
store.toggleSelection(widget.code.secret);
|
||||
} else {
|
||||
_copyCurrentOTPToClipboard();
|
||||
}
|
||||
},
|
||||
onDoubleTap: isMaskingEnabled
|
||||
? () {
|
||||
@@ -229,30 +247,16 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
onLongPress: widget.isReordering
|
||||
? null
|
||||
: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return BottomActionBarWidget(
|
||||
code: widget.code,
|
||||
showPin: !ignorePin,
|
||||
onEdit: () => _onEditPressed(true),
|
||||
onShare: () => _onSharePressed(true),
|
||||
onPin: () => _onPinPressed(true),
|
||||
onTrashed: () => _onTrashPressed(true),
|
||||
onDelete: () => _onDeletePressed(true),
|
||||
onRestore: () => _onRestoreClicked(true),
|
||||
onShowQR: () => _onShowQrPressed(true),
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
CodeDisplayStore.instance.toggleSelection(widget.code.secret);
|
||||
},
|
||||
child: getCardContents(l10n),
|
||||
child: getCardContents(l10n, isSelected: isSelected),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: widget.isCompactMode
|
||||
@@ -273,7 +277,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
),
|
||||
if (!widget.code.isTrashed)
|
||||
MenuItem(
|
||||
label: 'QR',
|
||||
label: context.l10n.qr,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
@@ -307,7 +311,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
const MenuDivider(),
|
||||
MenuItem(
|
||||
label: widget.code.isTrashed ? l10n.delete : l10n.trash,
|
||||
value: "Delete",
|
||||
value: l10n.delete,
|
||||
icon: widget.code.isTrashed
|
||||
? Icons.delete_forever
|
||||
: Icons.delete,
|
||||
@@ -403,54 +407,64 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getTopRow() {
|
||||
bool isCompactMode = widget.isCompactMode;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
safeDecode(widget.code.issuer).trim(),
|
||||
style: isCompactMode
|
||||
? Theme.of(context).textTheme.bodyMedium
|
||||
: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (!isCompactMode) const SizedBox(height: 2),
|
||||
Text(
|
||||
safeDecode(widget.code.account).trim(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: isCompactMode ? 12 : 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
Widget _getTopRow({required bool isSelected}) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
bool isCompactMode = widget.isCompactMode;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
color: colorScheme.primary400,
|
||||
size: isCompactMode ? 20 : 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
(widget.code.hasSynced != null && widget.code.hasSynced!) ||
|
||||
!hasConfiguredAccount
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(
|
||||
Icons.sync_disabled,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
Text(
|
||||
safeDecode(widget.code.issuer).trim(),
|
||||
style: isCompactMode
|
||||
? Theme.of(context).textTheme.bodyMedium
|
||||
: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
if (!isCompactMode) const SizedBox(height: 2),
|
||||
Text(
|
||||
safeDecode(widget.code.account).trim(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: isCompactMode ? 12 : 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_shouldShowLargeIcon ? const SizedBox.shrink() : _getIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
(widget.code.hasSynced != null && widget.code.hasSynced!) ||
|
||||
!hasConfiguredAccount
|
||||
? const SizedBox.shrink()
|
||||
: const Icon(
|
||||
Icons.sync_disabled,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_shouldShowLargeIcon ? const SizedBox.shrink() : _getIcon(),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getIcon() {
|
||||
final String iconData;
|
||||
@@ -478,7 +492,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
_getCurrentOTP(),
|
||||
confirmationMessage: context.l10n.copiedToClipboard,
|
||||
);
|
||||
_udateCodeMetadata().ignore();
|
||||
_updateCodeMetadata().ignore();
|
||||
}
|
||||
|
||||
void _copyNextToClipboard() {
|
||||
@@ -486,10 +500,10 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
_getNextTotp(),
|
||||
confirmationMessage: context.l10n.copiedNextToClipboard,
|
||||
);
|
||||
_udateCodeMetadata().ignore();
|
||||
_updateCodeMetadata().ignore();
|
||||
}
|
||||
|
||||
Future<void> _udateCodeMetadata() async {
|
||||
Future<void> _updateCodeMetadata() async {
|
||||
if (widget.sortKey == null) return;
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
@@ -502,7 +516,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
lastUsedAt: DateTime.now().microsecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
unawaited(CodeStore.instance.addCode(code));
|
||||
unawaited(CodeStore.instance.updateCode(widget.code, code));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -568,7 +582,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
),
|
||||
);
|
||||
if (code != null) {
|
||||
await CodeStore.instance.addCode(code);
|
||||
await CodeStore.instance.updateCode(widget.code, code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,7 +629,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
display: display.copyWith(pinned: !currentlyPinned),
|
||||
);
|
||||
unawaited(
|
||||
CodeStore.instance.addCode(code).then(
|
||||
CodeStore.instance.updateCode(widget.code,code).then(
|
||||
(value) => showToast(
|
||||
context,
|
||||
!currentlyPinned
|
||||
@@ -694,7 +708,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
final Code code = widget.code.copyWith(
|
||||
display: display.copyWith(trashed: true),
|
||||
);
|
||||
await CodeStore.instance.addCode(code);
|
||||
await CodeStore.instance.updateCode(widget.code, code);
|
||||
} catch (e) {
|
||||
logger.severe('Failed to trash code: ${e.toString()}');
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
@@ -718,7 +732,7 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
final Code code = widget.code.copyWith(
|
||||
display: display.copyWith(trashed: false),
|
||||
);
|
||||
await CodeStore.instance.addCode(code);
|
||||
await CodeStore.instance.updateCode(widget.code, code);
|
||||
} catch (e) {
|
||||
logger.severe('Failed to restore code: ${e.toString()}');
|
||||
if (mounted) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum DialogUserChoice { firstChoice, secondChoice }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
@@ -9,6 +8,7 @@ import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/components/models/custom_button_style.dart';
|
||||
import 'package:ente_auth/utils/debouncer.dart';
|
||||
import "package:ente_auth/utils/dialog_util.dart";
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/theme/effects.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
@@ -11,6 +10,7 @@ import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/components/separators.dart';
|
||||
import 'package:ente_auth/ui/components/text_input_widget.dart';
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
///Will return null if dismissed by tapping outside
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_child_widgets.dart';
|
||||
import 'package:ente_auth/utils/debouncer.dart';
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:expandable/expandable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/common/loading_widget.dart';
|
||||
import 'package:ente_auth/ui/components/separators.dart';
|
||||
import 'package:ente_auth/utils/debouncer.dart';
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/models/typedefs.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/models/execution_states.dart';
|
||||
import 'package:ente_auth/theme/colors.dart';
|
||||
import 'package:ente_auth/ui/common/loading_widget.dart';
|
||||
import 'package:ente_auth/utils/debouncer.dart';
|
||||
import 'package:ente_base/typedefs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef OnChangedCallBack = void Function(bool);
|
||||
|
||||
243
mobile/apps/auth/lib/ui/home/add_tag_sheet.dart
Normal file
243
mobile/apps/auth/lib/ui/home/add_tag_sheet.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
|
||||
class AddTagSheet extends StatefulWidget {
|
||||
final List<Code> selectedCodes;
|
||||
|
||||
const AddTagSheet({
|
||||
super.key,
|
||||
required this.selectedCodes,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddTagSheet> createState() => _AddTagSheetState();
|
||||
}
|
||||
|
||||
class _AddTagSheetState extends State<AddTagSheet> {
|
||||
List<String> _allTags = [];
|
||||
final Set<String> _selectedTagsInSheet = {};
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadInitialState();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialState() async {
|
||||
final allTagsFromServer = await CodeDisplayStore.instance.getAllTags();
|
||||
final initialTagsForSelection = <String>{};
|
||||
|
||||
for (final code in widget.selectedCodes) {
|
||||
initialTagsForSelection.addAll(code.display.tags);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_allTags = allTagsFromServer;
|
||||
_selectedTagsInSheet.addAll(initialTagsForSelection);
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDonePressed() async {
|
||||
final List<Future> updateFutures = [];
|
||||
for (final code in widget.selectedCodes) {
|
||||
final updatedCode = code.copyWith(
|
||||
display: code.display.copyWith(tags: _selectedTagsInSheet.toList()),
|
||||
);
|
||||
updateFutures.add(CodeStore.instance.updateCode(code, updatedCode));
|
||||
}
|
||||
await Future.wait(updateFutures);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCreateTagDialog() async {
|
||||
final textController = TextEditingController();
|
||||
final newTag = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.createNewTag),
|
||||
content: TextField(
|
||||
controller: textController,
|
||||
autofocus: true,
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.cancel,
|
||||
onTap: () async => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: context.l10n.create,
|
||||
isDisabled: textController.text.trim().isEmpty,
|
||||
onTap: () async => Navigator.of(context).pop(textController.text.trim()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (newTag != null && newTag.isNotEmpty) {
|
||||
setState(() {
|
||||
if (!_allTags.contains(newTag)) {
|
||||
_allTags.add(newTag);
|
||||
_allTags.sort();
|
||||
}
|
||||
_selectedTagsInSheet.add(newTag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = getEnteColorScheme(context);
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottomPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${widget.selectedCodes.length} ${context.l10n.selected}',
|
||||
style: textTheme.large,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.selectedCodes.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 16),
|
||||
itemBuilder: (context, index) {
|
||||
final code = widget.selectedCodes[index];
|
||||
final iconData =
|
||||
code.display.isCustomIcon ? code.display.iconID : code.issuer;
|
||||
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconUtils.instance.getIcon(context, iconData.trim(), width: 40),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
code.issuer,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.mini.copyWith(color: colorScheme.textMuted,),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(context.l10n.tags, style: textTheme.body),
|
||||
const SizedBox(height: 12),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 4.0,
|
||||
children: [
|
||||
..._allTags.map((tag) {
|
||||
final isSelected = _selectedTagsInSheet.contains(tag);
|
||||
return ChoiceChip(
|
||||
label: Text(tag),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTagsInSheet.add(tag);
|
||||
} else {
|
||||
_selectedTagsInSheet.remove(tag);
|
||||
}
|
||||
});
|
||||
},
|
||||
selectedColor: colorScheme.primary400,
|
||||
backgroundColor: colorScheme.fillFaint,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.white : colorScheme.textBase,
|
||||
),
|
||||
avatar: isSelected ? const Icon(Icons.check, color: Colors.white, size: 16) : null,
|
||||
side: BorderSide.none,
|
||||
shape: const StadiumBorder(),
|
||||
);
|
||||
}),
|
||||
ActionChip(
|
||||
avatar: const Icon(Icons.add, size: 18),
|
||||
label: Text(context.l10n.addNew),
|
||||
onPressed: _showCreateTagDialog,
|
||||
side: BorderSide.none,
|
||||
shape: const StadiumBorder(),
|
||||
backgroundColor: colorScheme.fillFaint,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.fillBase,
|
||||
foregroundColor: colorScheme.backgroundBase,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
onPressed: _onDonePressed,
|
||||
child: Text(context.l10n.done),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_events/event_bus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CoachMarkWidget extends StatelessWidget {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
class ScannerPage extends StatefulWidget {
|
||||
const ScannerPage({super.key});
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import 'package:ente_accounts/pages/change_email_dialog.dart';
|
||||
import 'package:ente_accounts/pages/delete_account_page.dart';
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_accounts/pages/recovery_key_page.dart';
|
||||
import 'package:ente_accounts/services/user_service.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/change_email_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/delete_account_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_key_page.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:ente_lock_screen/local_authentication_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AccountSectionWidget extends StatelessWidget {
|
||||
@@ -81,8 +82,10 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.update,
|
||||
return PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.update,
|
||||
const HomePage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -121,6 +124,7 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
Configuration.instance,
|
||||
recoveryKey,
|
||||
l10n.ok,
|
||||
showAppBar: true,
|
||||
@@ -151,8 +155,9 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final config = Configuration.instance;
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(context, const DeleteAccountPage());
|
||||
routeToPage(context, DeleteAccountPage(config));
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart';
|
||||
import 'package:ente_auth/ui/settings/data/export_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import_page.dart';
|
||||
import 'package:ente_auth/ui/settings/data/local_backup_settings_page.dart'; //for local backup
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -29,6 +30,10 @@ class DataSectionWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleLocalBackupClick(BuildContext context) async {
|
||||
await routeToPage(context, const LocalBackupSettingsPage());
|
||||
}
|
||||
|
||||
Column _getSectionOptions(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
List<Widget> children = [];
|
||||
@@ -86,10 +91,21 @@ class DataSectionWidget extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.localBackupSidebarTitle,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
await _handleLocalBackupClick(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/deduplication_service.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_lock_screen/local_authentication_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -2,8 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/export/ente.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/models/export/ente.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
@@ -14,6 +13,7 @@ import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/share_utils.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:ente_lock_screen/local_authentication_service.dart';
|
||||
import 'package:file_saver/file_saver.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'dart:io';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/models/export/ente.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
@@ -46,7 +45,7 @@ Future<void> showEncryptedImportInstruction(BuildContext context) async {
|
||||
if (result?.action != null && result!.action != ButtonAction.cancel) {
|
||||
if (result.action == ButtonAction.first) {
|
||||
await _pickEnteJsonFile(context);
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +57,9 @@ Future<void> _decryptExportData(
|
||||
final l10n = context.l10n;
|
||||
bool isPasswordIncorrect = false;
|
||||
int? importedCodeCount;
|
||||
|
||||
bool importHasRun = false;
|
||||
|
||||
await showTextInputDialog(
|
||||
context,
|
||||
title: l10n.passwordForDecryptingExport,
|
||||
@@ -67,6 +69,11 @@ Future<void> _decryptExportData(
|
||||
alwaysShowSuccessState: false,
|
||||
showOnlyLoadingState: true,
|
||||
onSubmit: (String password) async {
|
||||
if (importHasRun) {
|
||||
return;
|
||||
}
|
||||
importHasRun = true;
|
||||
|
||||
if (password.isEmpty) {
|
||||
showToast(context, l10n.passwordEmptyError);
|
||||
Future.delayed(const Duration(seconds: 0), () {
|
||||
@@ -78,6 +85,7 @@ Future<void> _decryptExportData(
|
||||
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
|
||||
try {
|
||||
await progressDialog.show();
|
||||
|
||||
final derivedKey = await CryptoUtil.deriveKey(
|
||||
utf8.encode(password),
|
||||
CryptoUtil.base642bin(enteAuthExport.kdfParams.salt),
|
||||
@@ -85,7 +93,6 @@ Future<void> _decryptExportData(
|
||||
enteAuthExport.kdfParams.opsLimit,
|
||||
);
|
||||
Uint8List? decryptedContent;
|
||||
// Encrypt the key with this derived key
|
||||
try {
|
||||
decryptedContent = await CryptoUtil.decryptData(
|
||||
CryptoUtil.base642bin(enteAuthExport.encryptedData),
|
||||
@@ -99,27 +106,62 @@ Future<void> _decryptExportData(
|
||||
}
|
||||
if (isPasswordIncorrect) {
|
||||
await progressDialog.hide();
|
||||
|
||||
Future.delayed(const Duration(seconds: 0), () {
|
||||
_decryptExportData(context, enteAuthExport, password: password);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
String content = utf8.decode(decryptedContent!);
|
||||
List<String> splitCodes = content.split("\n");
|
||||
final parsedCodes = [];
|
||||
for (final code in splitCodes) {
|
||||
|
||||
final List<Code> parsedCodes = [];
|
||||
for (final line in splitCodes) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
try {
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(code));
|
||||
String otpUrl = jsonDecode(line);
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
} catch (e) {
|
||||
Logger('EncryptedText').severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
|
||||
final List<Code> codesInApp = await CodeStore.instance.getAllCodes();
|
||||
final Map<String, Code> appCodesBySecret = { for (var code in codesInApp) code.secret: code };
|
||||
final List<Code> codesToImportAsNew = [];
|
||||
final List<Code> codesToUpdate = [];
|
||||
final Set<String> processedSecrets = {};
|
||||
|
||||
for (final codeFromFile in parsedCodes) {
|
||||
if (processedSecrets.contains(codeFromFile.secret)) {
|
||||
continue;
|
||||
}
|
||||
processedSecrets.add(codeFromFile.secret);
|
||||
if (appCodesBySecret.containsKey(codeFromFile.secret)) {
|
||||
final originalCodeInApp = appCodesBySecret[codeFromFile.secret]!;
|
||||
final updatedCode = codeFromFile.copyWith();
|
||||
updatedCode.generatedID = originalCodeInApp.generatedID;
|
||||
codesToUpdate.add(updatedCode);
|
||||
} else {
|
||||
codesToImportAsNew.add(codeFromFile);
|
||||
}
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
importedCodeCount = parsedCodes.length;
|
||||
|
||||
|
||||
if (codesToUpdate.isNotEmpty) {
|
||||
for (final codeToUpdate in codesToUpdate) {
|
||||
final originalCode = appCodesBySecret[codeToUpdate.secret]!;
|
||||
await CodeStore.instance.updateCode(originalCode, codeToUpdate, shouldSync: false);
|
||||
}
|
||||
}
|
||||
if (codesToImportAsNew.isNotEmpty) {
|
||||
for (final newCode in codesToImportAsNew) {
|
||||
await CodeStore.instance.addCode(newCode, shouldSync: false);
|
||||
}
|
||||
}
|
||||
|
||||
importedCodeCount = codesToImportAsNew.length + codesToUpdate.length;
|
||||
|
||||
await progressDialog.hide();
|
||||
} catch (e, s) {
|
||||
await progressDialog.hide();
|
||||
@@ -153,4 +195,4 @@ Future<void> _pickEnteJsonFile(BuildContext context) async {
|
||||
context.l10n.importFailureDescNew,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/lastpass_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/proton_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/two_fas_import.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import_page.dart';
|
||||
@@ -43,6 +44,9 @@ class ImportService {
|
||||
case ImportType.lastpass:
|
||||
await showLastpassImportInstruction(context);
|
||||
break;
|
||||
case ImportType.proton:
|
||||
await showProtonImportInstruction(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
171
mobile/apps/auth/lib/ui/settings/data/import/proton_import.dart
Normal file
171
mobile/apps/auth/lib/ui/settings/data/import/proton_import.dart
Normal file
@@ -0,0 +1,171 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<void> showProtonImportInstruction(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
final result = await showDialogWidget(
|
||||
context: context,
|
||||
title: l10n.importFromApp("Proton Authenticator"),
|
||||
body: l10n.importProtonAuthGuide,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: l10n.importSelectJsonFile,
|
||||
isInAlert: true,
|
||||
buttonSize: ButtonSize.large,
|
||||
buttonAction: ButtonAction.first,
|
||||
),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.cancel,
|
||||
buttonSize: ButtonSize.large,
|
||||
isInAlert: true,
|
||||
buttonAction: ButtonAction.second,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (result?.action != null && result!.action != ButtonAction.cancel) {
|
||||
if (result.action == ButtonAction.first) {
|
||||
await _pickProtonJsonFile(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickProtonJsonFile(BuildContext context) async {
|
||||
final l10n = context.l10n;
|
||||
FilePickerResult? result = await FilePicker.platform
|
||||
.pickFiles(dialogTitle: l10n.importSelectJsonFile);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final ProgressDialog progressDialog =
|
||||
createProgressDialog(context, l10n.pleaseWait);
|
||||
await progressDialog.show();
|
||||
try {
|
||||
String path = result.files.single.path!;
|
||||
int? count = await _processProtonExportFile(context, path, progressDialog);
|
||||
await progressDialog.hide();
|
||||
if (count != null) {
|
||||
await importSuccessDialog(context, count);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logger('ProtonImport')
|
||||
.severe('exception while processing proton import', e, s);
|
||||
await progressDialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.sorry,
|
||||
"${context.l10n.importFailureDescNew}\n Error: ${e.toString()}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int?> _processProtonExportFile(
|
||||
BuildContext context,
|
||||
String path,
|
||||
final ProgressDialog dialog,
|
||||
) async {
|
||||
File file = File(path);
|
||||
|
||||
final jsonString = await file.readAsString();
|
||||
final decodedJson = jsonDecode(jsonString);
|
||||
|
||||
// Validate that this is a Proton export
|
||||
if (decodedJson['version'] == null || decodedJson['entries'] == null) {
|
||||
await dialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
'Invalid Proton export',
|
||||
'The selected file is not a valid Proton Authenticator export.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsedCodes = <Code>[];
|
||||
final entries = decodedJson['entries'] as List;
|
||||
|
||||
for (var entry in entries) {
|
||||
try {
|
||||
final content = entry['content'];
|
||||
if (content == null) {
|
||||
continue; // Skip entries without content
|
||||
}
|
||||
|
||||
final entryType = content['entry_type'] as String?;
|
||||
if (entryType != 'Totp' && entryType != 'Steam') {
|
||||
// log warning
|
||||
Logger('ProtonImport').warning('Unsupported entry type: $entryType');
|
||||
continue; // Skip non-TOTP and non-Steam entries
|
||||
}
|
||||
|
||||
Code code;
|
||||
|
||||
if (entryType == 'Steam') {
|
||||
// Handle Steam entries with steam:// format
|
||||
final steamUri = content['uri'] as String?;
|
||||
if (steamUri == null || !steamUri.startsWith('steam://')) {
|
||||
continue; // Skip invalid Steam URIs
|
||||
}
|
||||
|
||||
final secret = steamUri.split('steam://')[1];
|
||||
final name = content['name'] as String? ?? '';
|
||||
|
||||
code = Code.fromAccountAndSecret(
|
||||
Type.steam,
|
||||
'', // Steam doesn't typically have separate account
|
||||
name, // Use name as issuer
|
||||
secret,
|
||||
null,
|
||||
Code.steamDigits,
|
||||
);
|
||||
} else {
|
||||
// Handle TOTP entries with otpauth:// format
|
||||
final otpUri = content['uri'] as String?;
|
||||
if (otpUri == null || !otpUri.startsWith('otpauth://')) {
|
||||
continue; // Skip invalid OTP URIs
|
||||
}
|
||||
// Create code from OTP auth URL
|
||||
code = Code.fromOTPAuthUrl(otpUri);
|
||||
}
|
||||
|
||||
// Add note if present
|
||||
final note = entry['note'] as String?;
|
||||
if (note != null && note.isNotEmpty) {
|
||||
code = code.copyWith(
|
||||
display: code.display.copyWith(note: note),
|
||||
);
|
||||
}
|
||||
|
||||
parsedCodes.add(code);
|
||||
} catch (e, s) {
|
||||
Logger('ProtonImport').warning('Failed to parse entry', e, s);
|
||||
// Continue processing other entries
|
||||
}
|
||||
}
|
||||
|
||||
// Add all parsed codes to the store
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
|
||||
// Trigger sync
|
||||
unawaited(AuthenticatorService.instance.onlineSync());
|
||||
|
||||
return parsedCodes.length;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ enum ImportType {
|
||||
twoFas,
|
||||
bitwarden,
|
||||
lastpass,
|
||||
proton,
|
||||
}
|
||||
|
||||
class ImportCodePage extends StatelessWidget {
|
||||
@@ -29,6 +30,7 @@ class ImportCodePage extends StatelessWidget {
|
||||
ImportType.aegis,
|
||||
ImportType.bitwarden,
|
||||
ImportType.googleAuthenticator,
|
||||
ImportType.proton,
|
||||
ImportType.ravio,
|
||||
ImportType.lastpass,
|
||||
];
|
||||
@@ -51,6 +53,8 @@ class ImportCodePage extends StatelessWidget {
|
||||
return 'Bitwarden';
|
||||
case ImportType.lastpass:
|
||||
return 'LastPass Authenticator';
|
||||
case ImportType.proton:
|
||||
return 'Proton Authenticator';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user