Compare commits
804 Commits
auth-v4.3.
...
photos-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87fae3e6d9 | ||
|
|
89cd360f93 | ||
|
|
f1274afdd4 | ||
|
|
0a5005d064 | ||
|
|
b54fe20520 | ||
|
|
204a046e0a | ||
|
|
4f21f1e94e | ||
|
|
039866cf3b | ||
|
|
c4b860a8fe | ||
|
|
1d1e01898f | ||
|
|
53f947b5f0 | ||
|
|
1e3a112c35 | ||
|
|
3dc23092a4 | ||
|
|
fd65e81079 | ||
|
|
eec0480618 | ||
|
|
fac5ab5079 | ||
|
|
051ce42ae6 | ||
|
|
297d4bdbf5 | ||
|
|
9f9ad19d4b | ||
|
|
4d26de8ffd | ||
|
|
f16846b82e | ||
|
|
b44ef9f68a | ||
|
|
cf28fddfb3 | ||
|
|
6c3b2ee25e | ||
|
|
2d8310460b | ||
|
|
0d5363c7a1 | ||
|
|
2f277bbffc | ||
|
|
cbf3340bf2 | ||
|
|
de918f42e6 | ||
|
|
6bd1547e09 | ||
|
|
aa70b2a437 | ||
|
|
13b74f387f | ||
|
|
d52e3894d8 | ||
|
|
780c2c2493 | ||
|
|
405c4d1258 | ||
|
|
5f498b01ee | ||
|
|
af5020e62c | ||
|
|
7ddf0f6fe1 | ||
|
|
5fa951ad4b | ||
|
|
e9ceb705f6 | ||
|
|
9dcd9d63b2 | ||
|
|
129e9f8f49 | ||
|
|
6f02df19c6 | ||
|
|
c8efc1a590 | ||
|
|
8b1a659d68 | ||
|
|
9114fbca27 | ||
|
|
e25d71a7d4 | ||
|
|
1018765f7c | ||
|
|
0fb984d031 | ||
|
|
4ad3560387 | ||
|
|
482b175324 | ||
|
|
894d7382e8 | ||
|
|
53cc78d3e3 | ||
|
|
a634500e55 | ||
|
|
ee8ecd456c | ||
|
|
d69a22a73e | ||
|
|
8b1af42cf0 | ||
|
|
41e7d0056b | ||
|
|
d170789446 | ||
|
|
afbbde5f2b | ||
|
|
4e3112a4f6 | ||
|
|
a4950ece53 | ||
|
|
1a01d759b0 | ||
|
|
175b51fdb3 | ||
|
|
a4ba2edc54 | ||
|
|
1a8a26e9e4 | ||
|
|
a2ddcfd34f | ||
|
|
95700f52f6 | ||
|
|
fc16638bfe | ||
|
|
5d375eb837 | ||
|
|
fe86d3bb34 | ||
|
|
77956d0f67 | ||
|
|
53a22a8d58 | ||
|
|
a5b178d283 | ||
|
|
e651c1e328 | ||
|
|
9069975bf0 | ||
|
|
5938e755ae | ||
|
|
6f7b3738b3 | ||
|
|
4ee9f45b3a | ||
|
|
c835a3d009 | ||
|
|
6d3e55a6d9 | ||
|
|
08b7986d70 | ||
|
|
067c8b2a76 | ||
|
|
d355d18acb | ||
|
|
0e2a0388ff | ||
|
|
2baf3a3dd7 | ||
|
|
854610dd48 | ||
|
|
61be57fef5 | ||
|
|
8d59d7e254 | ||
|
|
1bcf728b3a | ||
|
|
5797be3460 | ||
|
|
20f50e4816 | ||
|
|
5064ebf4d3 | ||
|
|
eb783f0fff | ||
|
|
ca8f310868 | ||
|
|
2e0a2802e7 | ||
|
|
7868c2e16e | ||
|
|
ace5dc04e2 | ||
|
|
4249491730 | ||
|
|
a958380a1d | ||
|
|
b0d940e65b | ||
|
|
5b8472b5a9 | ||
|
|
6bb7627d47 | ||
|
|
b00c406b09 | ||
|
|
ff9494d438 | ||
|
|
b24b5893ae | ||
|
|
9716ff80c4 | ||
|
|
2808f72233 | ||
|
|
5dd097ee09 | ||
|
|
be9fddf1d4 | ||
|
|
f9b3f6e9eb | ||
|
|
dcb73abdec | ||
|
|
a14c6f4d26 | ||
|
|
7afdfe6ed9 | ||
|
|
8ff8981b76 | ||
|
|
b89f247f42 | ||
|
|
c3e5a037c0 | ||
|
|
70a1894071 | ||
|
|
42453675b2 | ||
|
|
bdd09e12d8 | ||
|
|
407ad41520 | ||
|
|
185759d234 | ||
|
|
860760784a | ||
|
|
de10292a84 | ||
|
|
95f10e5a45 | ||
|
|
eabacf24ad | ||
|
|
61ffcfdc93 | ||
|
|
0ade0a2807 | ||
|
|
1e2fb13908 | ||
|
|
856e126bc8 | ||
|
|
845d014945 | ||
|
|
d8b54f5211 | ||
|
|
bc059c861f | ||
|
|
b52ee5bbfb | ||
|
|
93c85a57e4 | ||
|
|
e01826217d | ||
|
|
b79b7ff3ef | ||
|
|
7d52e3d852 | ||
|
|
9a647d6f78 | ||
|
|
6e99206523 | ||
|
|
d7af21aa84 | ||
|
|
ced1f6e164 | ||
|
|
4ea3989a33 | ||
|
|
641a99b823 | ||
|
|
7b48dbc1ad | ||
|
|
54c69e7aa5 | ||
|
|
f8bd8c9955 | ||
|
|
60246be861 | ||
|
|
d71016500a | ||
|
|
f44c2d14c7 | ||
|
|
9c26f4040a | ||
|
|
79e048b4b7 | ||
|
|
5c0ce038d1 | ||
|
|
2903388c94 | ||
|
|
331a65d2a0 | ||
|
|
6c6ab8f463 | ||
|
|
441a884314 | ||
|
|
3372d83c5d | ||
|
|
0cf50513cc | ||
|
|
7ccf473190 | ||
|
|
c1f7a01ed2 | ||
|
|
256243e273 | ||
|
|
dc9dc5d8f9 | ||
|
|
6969385089 | ||
|
|
87a1c9417e | ||
|
|
6d13ff5151 | ||
|
|
0b5317867f | ||
|
|
a32c8116a2 | ||
|
|
d8cd81c702 | ||
|
|
18e7bbd1ed | ||
|
|
63850df06a | ||
|
|
886ab6d106 | ||
|
|
d17296216c | ||
|
|
55ee8b90b9 | ||
|
|
634561347f | ||
|
|
575abdb8eb | ||
|
|
e998502b53 | ||
|
|
2ada68e837 | ||
|
|
28822a8dc1 | ||
|
|
deaa9a703d | ||
|
|
4510edf8bd | ||
|
|
7af59a1ecf | ||
|
|
e003c783f5 | ||
|
|
379d2487bd | ||
|
|
8cdbb737dc | ||
|
|
d528d97a0f | ||
|
|
682e4a913f | ||
|
|
a72041b8ba | ||
|
|
ab1a8aa592 | ||
|
|
c37a0339d2 | ||
|
|
3b60f4954b | ||
|
|
b2ea248a5c | ||
|
|
1bda14fb6f | ||
|
|
54e8d6392d | ||
|
|
afa9e03743 | ||
|
|
a4eaf04a33 | ||
|
|
d30f0fba04 | ||
|
|
b2855cfd72 | ||
|
|
06d260f40a | ||
|
|
fb16346b0d | ||
|
|
41b1638838 | ||
|
|
702b3a8868 | ||
|
|
3572c4328d | ||
|
|
1c2b8061dc | ||
|
|
a9edcead06 | ||
|
|
5a574c69d3 | ||
|
|
192905b21e | ||
|
|
52a533a1e1 | ||
|
|
0314e94359 | ||
|
|
cbef1a9145 | ||
|
|
822c33940e | ||
|
|
eb29c48f0e | ||
|
|
c77b4f176c | ||
|
|
afcc7b1e46 | ||
|
|
84dda89e15 | ||
|
|
4bbc0d1f46 | ||
|
|
aa6d6f4e77 | ||
|
|
ff26dd5652 | ||
|
|
b68d95d481 | ||
|
|
ec1b54cbb1 | ||
|
|
459540fe7a | ||
|
|
2255ea1b92 | ||
|
|
ac704f1082 | ||
|
|
4db2e42ee3 | ||
|
|
f5949f5bd4 | ||
|
|
c4feb4b764 | ||
|
|
29bab5705b | ||
|
|
2ebeed3b6f | ||
|
|
a0dbcd3dbe | ||
|
|
81137652d4 | ||
|
|
4e1418b11a | ||
|
|
91268341be | ||
|
|
fb8fc051a9 | ||
|
|
d99914c4e9 | ||
|
|
736b3fc613 | ||
|
|
ef7f45aa3d | ||
|
|
4dc741151b | ||
|
|
e4471af4cb | ||
|
|
057df349b7 | ||
|
|
24c7b49132 | ||
|
|
5df009c7c7 | ||
|
|
1d276c795c | ||
|
|
bc762b972f | ||
|
|
36e4c06dd6 | ||
|
|
ceb3d3fe42 | ||
|
|
1dc806d270 | ||
|
|
8d03df5c36 | ||
|
|
a1ef8d33d3 | ||
|
|
4f347c1afd | ||
|
|
8171d56168 | ||
|
|
7e7751b5be | ||
|
|
d19a0fccda | ||
|
|
5b38ef394b | ||
|
|
ad87470c25 | ||
|
|
67140fe7f2 | ||
|
|
b372ba47ba | ||
|
|
24c66a9b6b | ||
|
|
201ef60f07 | ||
|
|
89294f2a76 | ||
|
|
4772557f7a | ||
|
|
073d2c5684 | ||
|
|
e022e7ae5b | ||
|
|
2cdeb88b4d | ||
|
|
ca319e501e | ||
|
|
05898dfbe2 | ||
|
|
c129cc15b5 | ||
|
|
a683883733 | ||
|
|
69b575cc66 | ||
|
|
63a4972839 | ||
|
|
6188578d18 | ||
|
|
5a4d8950af | ||
|
|
5007204944 | ||
|
|
4a19fc077e | ||
|
|
2b29f55587 | ||
|
|
4cc8ff2fb1 | ||
|
|
de29246304 | ||
|
|
8deb52301a | ||
|
|
16a20e8b0d | ||
|
|
3824bfbdd5 | ||
|
|
c996c794fd | ||
|
|
ef245e5c02 | ||
|
|
4dc6890afc | ||
|
|
87195f3801 | ||
|
|
8ce45a4fa8 | ||
|
|
520e5d4ae7 | ||
|
|
2a8e167e42 | ||
|
|
2f7bde36bd | ||
|
|
ace375b7f6 | ||
|
|
cde6ebfa39 | ||
|
|
a1e56a457f | ||
|
|
a4ebf972e1 | ||
|
|
7d5bed0493 | ||
|
|
d449bd0f90 | ||
|
|
5d14ca8439 | ||
|
|
619f6795e2 | ||
|
|
04cd1d3bb3 | ||
|
|
0960f189ce | ||
|
|
734b836a7a | ||
|
|
91447cdc77 | ||
|
|
961501a6fb | ||
|
|
914893eae6 | ||
|
|
7a10f4c145 | ||
|
|
092f64c3ca | ||
|
|
bdecb04398 | ||
|
|
e25418e5a6 | ||
|
|
a062c1ccc3 | ||
|
|
d1ae4d52dd | ||
|
|
3c532cd4f4 | ||
|
|
013389c696 | ||
|
|
9ca418d915 | ||
|
|
a52a3e5e57 | ||
|
|
2d739ef4de | ||
|
|
300300a8b9 | ||
|
|
be00cbaa51 | ||
|
|
43641b0b9e | ||
|
|
43cdd10e85 | ||
|
|
539145d38d | ||
|
|
08bb2f25fb | ||
|
|
55264445b2 | ||
|
|
bdadf8dc9c | ||
|
|
1c3428d89d | ||
|
|
6e4a856ea4 | ||
|
|
40ff361af1 | ||
|
|
11fae5e87c | ||
|
|
465e69b254 | ||
|
|
63b0cee589 | ||
|
|
574eea58fc | ||
|
|
f848fe0938 | ||
|
|
138310b8f8 | ||
|
|
3d63ded84d | ||
|
|
b7cd55aec3 | ||
|
|
aadab316f6 | ||
|
|
8bad8b87b1 | ||
|
|
2ff69f661e | ||
|
|
10c65f13c8 | ||
|
|
761c976d7e | ||
|
|
423a7eec37 | ||
|
|
1f7e74131b | ||
|
|
c8b23f80e2 | ||
|
|
191db47d79 | ||
|
|
34621dd00f | ||
|
|
d127199ade | ||
|
|
f44e90801f | ||
|
|
ded497d421 | ||
|
|
a3e48706de | ||
|
|
2709f69f2a | ||
|
|
2b32cd4277 | ||
|
|
57934f612e | ||
|
|
eae5e12c71 | ||
|
|
aef50f3f95 | ||
|
|
6772027c27 | ||
|
|
f386a0a72e | ||
|
|
9c94bcffaa | ||
|
|
6b703e9601 | ||
|
|
22bae0292d | ||
|
|
4c7121fd6c | ||
|
|
f53745bbb0 | ||
|
|
1c6e343994 | ||
|
|
949807bc97 | ||
|
|
fea46532f9 | ||
|
|
8824df29d4 | ||
|
|
2da2616ec1 | ||
|
|
2144f57ee0 | ||
|
|
476fe1b624 | ||
|
|
fb7fc53263 | ||
|
|
a842cdfe4e | ||
|
|
d5de1b5ce2 | ||
|
|
a1801435bd | ||
|
|
bc02238fd8 | ||
|
|
49ef8a693b | ||
|
|
6f585c3ad0 | ||
|
|
18f9f0048a | ||
|
|
6a7ba4156f | ||
|
|
30995b3b73 | ||
|
|
ff705280c7 | ||
|
|
6ff6594a81 | ||
|
|
69f12125ec | ||
|
|
862b11c530 | ||
|
|
e40afac212 | ||
|
|
8c66d9dac0 | ||
|
|
a880726f16 | ||
|
|
b2ab0679b4 | ||
|
|
d63ecf518e | ||
|
|
c5cdaf3e0b | ||
|
|
61f1cd6952 | ||
|
|
e6d4a779c5 | ||
|
|
14087ec5e9 | ||
|
|
15daa9e453 | ||
|
|
73728e60e0 | ||
|
|
5c925c4265 | ||
|
|
83bf336101 | ||
|
|
d5b9a0a92b | ||
|
|
40d84f10a8 | ||
|
|
bffd4d83a5 | ||
|
|
59a534225c | ||
|
|
810ee3d9fe | ||
|
|
9605637e50 | ||
|
|
9016394ccc | ||
|
|
a518bbd608 | ||
|
|
1c4ebcccb1 | ||
|
|
793fd5ba39 | ||
|
|
9b1eacf736 | ||
|
|
cae9988c9a | ||
|
|
538a5df32d | ||
|
|
853b916cf1 | ||
|
|
93d6f58660 | ||
|
|
7b145f0898 | ||
|
|
add09a601d | ||
|
|
cab711d88e | ||
|
|
c392bd2fd2 | ||
|
|
5dda596544 | ||
|
|
8101bee2fd | ||
|
|
c234bc7be8 | ||
|
|
27fd372d62 | ||
|
|
66f23283c1 | ||
|
|
8d7bc81c20 | ||
|
|
5e3ebd4a60 | ||
|
|
f4a4f71135 | ||
|
|
41e870f7a0 | ||
|
|
0342e1ef56 | ||
|
|
efed42ce4a | ||
|
|
185c5f5c43 | ||
|
|
6f5b33ea01 | ||
|
|
ef9d925686 | ||
|
|
d6a8373e5d | ||
|
|
e84f46f435 | ||
|
|
c1193be61c | ||
|
|
9b460ca1dc | ||
|
|
bd0ad0d1b4 | ||
|
|
d19322c1ae | ||
|
|
e71f5cecc7 | ||
|
|
622a368b45 | ||
|
|
72aec4bc5a | ||
|
|
c899725ed1 | ||
|
|
e15eb8d7c7 | ||
|
|
b38c49502b | ||
|
|
0fdee342eb | ||
|
|
0641ce389d | ||
|
|
f34ac356e9 | ||
|
|
120f9cef4d | ||
|
|
9b4247680a | ||
|
|
5d69376b54 | ||
|
|
64260896a0 | ||
|
|
2af46f62c8 | ||
|
|
a53701bc41 | ||
|
|
7a408a6242 | ||
|
|
6b9885c5d7 | ||
|
|
caa453d49d | ||
|
|
dd80acd4f4 | ||
|
|
5aa7682812 | ||
|
|
cb9a88e636 | ||
|
|
3a32659dd4 | ||
|
|
d5b5a26d9a | ||
|
|
d2980abb7a | ||
|
|
1a2f606d94 | ||
|
|
a101dba6cd | ||
|
|
491de296ca | ||
|
|
ab2719a79c | ||
|
|
a614636789 | ||
|
|
48e9032ac1 | ||
|
|
1d2e18444c | ||
|
|
7f9e01a841 | ||
|
|
6933c77f36 | ||
|
|
61cacdddc0 | ||
|
|
936d578093 | ||
|
|
167a81f121 | ||
|
|
b550ee6b15 | ||
|
|
54feb7b2f9 | ||
|
|
f9dad575ec | ||
|
|
a39948ccc3 | ||
|
|
dfabd648d5 | ||
|
|
21e5bbb0fd | ||
|
|
10a0165a0f | ||
|
|
07c640cf90 | ||
|
|
9fa13508b8 | ||
|
|
1c099a60e8 | ||
|
|
efbdaef9ce | ||
|
|
65ddea0f7f | ||
|
|
ed15b47f1b | ||
|
|
6496660718 | ||
|
|
691d31d0a9 | ||
|
|
1f40901fd1 | ||
|
|
4982c1209e | ||
|
|
9170c80b26 | ||
|
|
1b4cb2ed99 | ||
|
|
45fe850afc | ||
|
|
6ae8abb492 | ||
|
|
f077213c62 | ||
|
|
92a8db53bd | ||
|
|
c812b80887 | ||
|
|
62c1f0c6ac | ||
|
|
cc486983ab | ||
|
|
a103bd4cca | ||
|
|
c56d9b312d | ||
|
|
6dda5d8762 | ||
|
|
3e9d78c866 | ||
|
|
c3eb55b258 | ||
|
|
0666bf56df | ||
|
|
fcd8298e10 | ||
|
|
62bd2d13d6 | ||
|
|
4263906c61 | ||
|
|
72acefadd4 | ||
|
|
2be8db783c | ||
|
|
f8e90e765f | ||
|
|
9424d26f55 | ||
|
|
bb352f3266 | ||
|
|
85d55c7d26 | ||
|
|
6062c20251 | ||
|
|
8861e0562f | ||
|
|
5e4de0793a | ||
|
|
80d36b5a91 | ||
|
|
f22ad9611f | ||
|
|
8e038dda14 | ||
|
|
9cafa72ae3 | ||
|
|
a5eba40f29 | ||
|
|
0c9d7321eb | ||
|
|
d98d220019 | ||
|
|
5fc5d0ef48 | ||
|
|
ce6b0da630 | ||
|
|
d1d91338af | ||
|
|
790c022730 | ||
|
|
be65f0fba8 | ||
|
|
3677d53ea9 | ||
|
|
84017c7397 | ||
|
|
f92101eaf8 | ||
|
|
1d3ca81308 | ||
|
|
e9f22cff93 | ||
|
|
e250759999 | ||
|
|
d5740c4b66 | ||
|
|
07bd28381d | ||
|
|
8c8c9d7ffa | ||
|
|
d06016dddc | ||
|
|
f4531ef088 | ||
|
|
56772521e0 | ||
|
|
244ec87a9a | ||
|
|
043d0936ef | ||
|
|
9129910981 | ||
|
|
4194c53d08 | ||
|
|
9d5960c6fe | ||
|
|
f088c24abe | ||
|
|
e197423c1d | ||
|
|
b22bc93e57 | ||
|
|
236301dc16 | ||
|
|
b4a60fd2f4 | ||
|
|
079ac37d1f | ||
|
|
3deb138b77 | ||
|
|
5b9b328c99 | ||
|
|
e541f0522d | ||
|
|
93413687c9 | ||
|
|
48ef4df187 | ||
|
|
96d748dc87 | ||
|
|
9d28fa99a3 | ||
|
|
f9cce787f7 | ||
|
|
dcc02db296 | ||
|
|
c0fd71a668 | ||
|
|
116f22a853 | ||
|
|
501b541134 | ||
|
|
1785baf4af | ||
|
|
b08d8de1c8 | ||
|
|
0284287c9c | ||
|
|
be2665f57f | ||
|
|
b721a84889 | ||
|
|
442d6526be | ||
|
|
08346e5bcd | ||
|
|
e35b4eac40 | ||
|
|
7c6da77c21 | ||
|
|
ab59a15769 | ||
|
|
45e07f3be0 | ||
|
|
a2df4fb48a | ||
|
|
5a2ba82fff | ||
|
|
6a907d0f7d | ||
|
|
20dfec2e67 | ||
|
|
96c3880e38 | ||
|
|
2adabc1e24 | ||
|
|
a6ca7a5792 | ||
|
|
6abea7ae6d | ||
|
|
429b6be368 | ||
|
|
ed9af710fe | ||
|
|
f8ef263e6d | ||
|
|
fae47f102e | ||
|
|
240bfdd296 | ||
|
|
672123d746 | ||
|
|
51d55ee92b | ||
|
|
cc3f20831a | ||
|
|
a282f82909 | ||
|
|
2fde22272e | ||
|
|
d63da04392 | ||
|
|
7cd3f8e2ac | ||
|
|
9517514396 | ||
|
|
41a4f3d286 | ||
|
|
91fefa7eb9 | ||
|
|
63b9a09a2d | ||
|
|
d1a2efff0e | ||
|
|
00d723ca7c | ||
|
|
47b2e0d0ab | ||
|
|
ac9dbd4ee1 | ||
|
|
8b6f6346be | ||
|
|
73ad6b45fb | ||
|
|
020f8cea23 | ||
|
|
d52accdcc9 | ||
|
|
975c7ae8d1 | ||
|
|
c4463d6251 | ||
|
|
e96888d2c9 | ||
|
|
03df804868 | ||
|
|
685e0099c8 | ||
|
|
4a924a7296 | ||
|
|
1f4beab764 | ||
|
|
4fd121adf1 | ||
|
|
a942e68d27 | ||
|
|
ece63f7a8d | ||
|
|
dcdc26e4e8 | ||
|
|
ffa8ccd0d3 | ||
|
|
01f612e450 | ||
|
|
70f80c7b57 | ||
|
|
ab875683ed | ||
|
|
638108a819 | ||
|
|
dec193599f | ||
|
|
4913746a31 | ||
|
|
0966ab7d19 | ||
|
|
83ae436008 | ||
|
|
8f2a2caac1 | ||
|
|
7cc6319d5f | ||
|
|
eabe213207 | ||
|
|
5b7f821a26 | ||
|
|
939ab4398c | ||
|
|
c4effd4ef4 | ||
|
|
bcf1a044cc | ||
|
|
2dd9a080c6 | ||
|
|
57a4e6dd15 | ||
|
|
93cd2d30e4 | ||
|
|
f65db123f2 | ||
|
|
d697f22896 | ||
|
|
65faa98621 | ||
|
|
e4a0125f1c | ||
|
|
98d06f975c | ||
|
|
d0ebe65af4 | ||
|
|
08d936be45 | ||
|
|
71596ebd8e | ||
|
|
8ededf54c7 | ||
|
|
0520fcd7ec | ||
|
|
b6173d6c1b | ||
|
|
6fa07e12db | ||
|
|
804dacef91 | ||
|
|
97350005c9 | ||
|
|
5f36a23f4e | ||
|
|
d336541d2c | ||
|
|
780ea55ca8 | ||
|
|
38663088b1 | ||
|
|
eb9987d2c0 | ||
|
|
cf4084380c | ||
|
|
691469ef5e | ||
|
|
8106bc6940 | ||
|
|
deb68d5cfc | ||
|
|
f4e158dab6 | ||
|
|
e8b0d9c25b | ||
|
|
43ce9edb95 | ||
|
|
3b4c7095c4 | ||
|
|
d95bb0785f | ||
|
|
d53281500b | ||
|
|
44199093f0 | ||
|
|
18442e25fc | ||
|
|
0fa7245144 | ||
|
|
4fc7347cb7 | ||
|
|
4840a44c4d | ||
|
|
109ac573c9 | ||
|
|
23559252e6 | ||
|
|
31b31b1a52 | ||
|
|
8333e2ad7b | ||
|
|
cb5c9f3170 | ||
|
|
7b2e6cb1bd | ||
|
|
d18d939489 | ||
|
|
b3376f27aa | ||
|
|
327bda5b30 | ||
|
|
c3b1da2a7e | ||
|
|
f238b55df3 | ||
|
|
d15a034869 | ||
|
|
7b3ae417e8 | ||
|
|
d35f898b70 | ||
|
|
6730c0c682 | ||
|
|
c2c7ac8b23 | ||
|
|
e322958b25 | ||
|
|
0d660f239f | ||
|
|
c4a50fc9fb | ||
|
|
2f670e316b | ||
|
|
f47a6f7b42 | ||
|
|
8856ad1520 | ||
|
|
3513b51477 | ||
|
|
bd8fc08b7c | ||
|
|
ae925a240e | ||
|
|
adf1379b9e | ||
|
|
e8158ef45a | ||
|
|
71d6aed1aa | ||
|
|
6627f77d92 | ||
|
|
4fa0bf76e8 | ||
|
|
92a9b34836 | ||
|
|
10d7162d6e | ||
|
|
2a1b8ae18e | ||
|
|
5abf2cb35e | ||
|
|
367170be95 | ||
|
|
4d7cfee60f | ||
|
|
29152d1f85 | ||
|
|
6b4ffa4822 | ||
|
|
2883f4bed6 | ||
|
|
c96275cdd1 | ||
|
|
9db8324ffd | ||
|
|
0c664b94b9 | ||
|
|
c087e419d5 | ||
|
|
5ba5cae5ef | ||
|
|
4ea211d923 | ||
|
|
8d8202adab | ||
|
|
267f93e41e | ||
|
|
260ec952b4 | ||
|
|
5e311c2813 | ||
|
|
1d3268916f | ||
|
|
73192cd0fd | ||
|
|
9c886b3fa3 | ||
|
|
017832f11e | ||
|
|
67e76bc42f | ||
|
|
9a6579c55c | ||
|
|
17c0cdef14 | ||
|
|
dd0cc71f36 | ||
|
|
21fd6ab463 | ||
|
|
6e2142c605 | ||
|
|
16338682ed | ||
|
|
a7e8d3dfa6 | ||
|
|
6e9014b915 | ||
|
|
b5e7a3f83f | ||
|
|
d8d76f452d | ||
|
|
c2e475c666 | ||
|
|
9a4bc898f0 | ||
|
|
ca92aa8c62 | ||
|
|
56c6d7ed3c | ||
|
|
6ee4bce676 | ||
|
|
ff3f01af56 | ||
|
|
b5ba81e22b | ||
|
|
d5aab7c6df | ||
|
|
2749457611 | ||
|
|
883b14e96a | ||
|
|
59d7e0acac | ||
|
|
68ac3503ed | ||
|
|
58649db181 | ||
|
|
92ca4eeb15 | ||
|
|
d3e06e6cc9 | ||
|
|
3cef3e9bdc | ||
|
|
d318952feb | ||
|
|
6d8051dfa0 | ||
|
|
d198f0c273 | ||
|
|
a88249de09 | ||
|
|
14b70ce66e | ||
|
|
d9a5fbfe00 | ||
|
|
0a3abb20a1 | ||
|
|
9f9288a5c0 | ||
|
|
100c1d3803 | ||
|
|
408cc05f7c | ||
|
|
9f70aab9b5 | ||
|
|
f816166743 | ||
|
|
3aba4fad47 | ||
|
|
60137585d1 | ||
|
|
59316c263f | ||
|
|
44682404ff | ||
|
|
83c864df2b | ||
|
|
184882fae2 | ||
|
|
e59e600a35 | ||
|
|
6f5c5a0b06 | ||
|
|
3d4ff93e65 | ||
|
|
d88621ab5a | ||
|
|
95e6a86b10 | ||
|
|
e5d63fe9e7 | ||
|
|
1265002d5a | ||
|
|
c7aecc9b30 | ||
|
|
dba837c62c | ||
|
|
e5bdd74fa9 | ||
|
|
0f31278965 | ||
|
|
be3e434bec | ||
|
|
9d9a7b548d | ||
|
|
3708819e6b | ||
|
|
389220357e | ||
|
|
d9925f29d8 | ||
|
|
0dab15b703 | ||
|
|
2449dbe0cd | ||
|
|
7f96a11e07 | ||
|
|
d7cb5c29cf | ||
|
|
399ecdfd7d | ||
|
|
f4580c8fdf | ||
|
|
13f78ecc19 | ||
|
|
9267c4012b | ||
|
|
a8e80717aa | ||
|
|
8e552c57bb | ||
|
|
28f03d3514 | ||
|
|
c7047ab964 | ||
|
|
daa3fcd354 | ||
|
|
0057e71e02 | ||
|
|
50fabee1e0 | ||
|
|
227d76db29 | ||
|
|
9207b0c7b8 | ||
|
|
9b8c48ca6e | ||
|
|
dc05e254cb | ||
|
|
f047757c63 | ||
|
|
f9711e09a1 | ||
|
|
a5644f292e | ||
|
|
f3a88dc3fa | ||
|
|
9ef2a5fc62 | ||
|
|
36ab2f05df | ||
|
|
723511540c |
6
.github/workflows/auth-release.yml
vendored
@@ -83,7 +83,7 @@ jobs:
|
||||
# disable this step if release tag contains nightly or beta
|
||||
if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta')
|
||||
run: |
|
||||
flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore
|
||||
flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore --dart-define=cronetHttpNoPlay=true
|
||||
env:
|
||||
SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks"
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -117,7 +117,9 @@ jobs:
|
||||
mv dist/**/*-*-linux.deb artifacts/ente-${{ github.ref_name }}-x86_64.deb
|
||||
|
||||
- name: Generate checksums
|
||||
run: sha256sum artifacts/ente-* >> artifacts/sha256sum-rpm-appimage
|
||||
run: |
|
||||
sha256sum artifacts/ente-auth-*.apk >> artifacts/sha256sum-apk
|
||||
sha256sum artifacts/ente-auth-*.deb artifacts/ente-auth-*.rpm artifacts/ente-auth-*.AppImage >> artifacts/sha256sum-linux
|
||||
|
||||
- name: Create a draft GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
|
||||
77
.github/workflows/mobile-internal-release-rust.yml
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
name: "Internal release (photos)"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manually running the action
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.24.3"
|
||||
RUST_VERSION: "1.85.1"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mobile
|
||||
|
||||
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/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 available for Photos"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)"
|
||||
color: 0x00ff00
|
||||
@@ -63,6 +63,6 @@ jobs:
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }}
|
||||
nodetail: true
|
||||
title: "🏆 Internal release available for Photos"
|
||||
title: "🏆 Internal release Photos (Branch: ${{ github.ref_name }})"
|
||||
description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)"
|
||||
color: 0x00ff00
|
||||
|
||||
@@ -43,7 +43,13 @@
|
||||
"title": "Anycoin Direct",
|
||||
"slug": "anycoindirect"
|
||||
},
|
||||
{
|
||||
{
|
||||
"title": "AR24",
|
||||
"altNames": [
|
||||
"Docaposte AR24"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Aruba",
|
||||
"slug": "aruba",
|
||||
"hex": "ef8a33"
|
||||
@@ -132,6 +138,10 @@
|
||||
"Binance US"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Bitazza",
|
||||
"slug": "bitazza"
|
||||
},
|
||||
{
|
||||
"title": "Bitkub",
|
||||
"slug": "bitkub"
|
||||
@@ -188,7 +198,9 @@
|
||||
"slug": "blue_sky"
|
||||
},
|
||||
{
|
||||
"title": "bonify"
|
||||
"title": "bonify",
|
||||
"slug": "bonify",
|
||||
"altNames": ["bonify.de"]
|
||||
},
|
||||
{
|
||||
"title": "Booking",
|
||||
@@ -299,6 +311,10 @@
|
||||
"title": "CSGORoll",
|
||||
"slug": "csgoroll"
|
||||
},
|
||||
{
|
||||
"title": "Cryptee",
|
||||
"slug": "cryptee"
|
||||
},
|
||||
{
|
||||
"title": "Cwallet",
|
||||
"altNames": [
|
||||
@@ -431,6 +447,9 @@
|
||||
"title": "Finanzfluss",
|
||||
"slug": "finanzfluss"
|
||||
},
|
||||
{
|
||||
"title": "Finary"
|
||||
},
|
||||
{
|
||||
"title": "Firefox",
|
||||
"slug": "mozilla"
|
||||
@@ -733,6 +752,7 @@
|
||||
{
|
||||
"title": "Mistral",
|
||||
"altNames": [
|
||||
"Le Chat",
|
||||
"Mistral AI",
|
||||
"MistralAI"
|
||||
]
|
||||
@@ -877,11 +897,15 @@
|
||||
"欧易"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"title": "OnShape",
|
||||
"slug": "onshape",
|
||||
"hex": "7abb5e"
|
||||
},
|
||||
{
|
||||
"title": "Oracle Cloud",
|
||||
"slug": "oracle_cloud"
|
||||
},
|
||||
{
|
||||
"title": "Parqet",
|
||||
"slug": "parqet"
|
||||
@@ -1001,7 +1025,7 @@
|
||||
"title": "RealMe",
|
||||
"slug": "realme"
|
||||
},
|
||||
{
|
||||
{
|
||||
"title": "RealVNC",
|
||||
"slug": "realvnc",
|
||||
"hex": "488aec"
|
||||
@@ -1355,4 +1379,4 @@
|
||||
"title": "CoinSpot"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
auth/assets/custom-icons/icons/ar24.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="#0000FF" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M139.63 306.55H125.64C123.41 306.55 121.53 306.5 119.99 306.39C118.45 306.28 117.13 305.99 116.01 305.51C114.9 305.04 113.92 304.32 113.08 303.36C112.22 302.41 111.38 301.09 110.53 299.39L103.85 286.35H35.47L25.29 306.55H0L58.37 194.11H81.26L139.63 306.55ZM93.2 265.36L69.66 218.6L46.12 265.36H93.2Z"/>
|
||||
<path d="M265.23 306.55H245.67C241.96 306.55 238.93 306.07 236.6 305.12C234.27 304.16 232.31 302.68 230.72 300.67L206.17 270.76H177.48V306.55H153.95V195.23C156.92 195.12 160.16 195.04 163.66 194.99C167.17 194.93 170.77 194.86 174.5 194.75C178.21 194.64 181.92 194.57 185.64 194.51C189.36 194.46 192.86 194.43 196.15 194.43C209.74 194.43 220.97 195.41 229.84 197.37C238.71 199.34 246 202.91 251.74 208.1C258.11 214.04 261.3 222.1 261.3 232.28C261.3 237.37 260.61 241.85 259.23 245.72C257.84 249.59 255.88 252.96 253.33 255.81C250.78 258.68 247.7 261.09 244.09 263.05C240.49 265.02 236.45 266.63 231.99 267.9L265.23 306.55ZM196.25 250.25C199.75 250.25 203.27 250.22 206.82 250.17C210.38 250.12 213.74 249.88 216.93 249.45C220.1 249.03 223.02 248.36 225.67 247.46C228.32 246.55 230.5 245.24 232.19 243.54C233.67 241.94 234.82 240.29 235.61 238.59C236.4 236.89 236.81 234.7 236.81 232.03C236.81 230.23 236.45 228.47 235.77 226.77C235.08 225.06 234.15 223.62 232.99 222.45C231.4 220.85 229.44 219.6 227.11 218.7C224.77 217.79 222.1 217.12 219.07 216.7C216.05 216.27 212.63 216.03 208.81 215.98C205 215.93 200.81 215.9 196.25 215.9H187.18C183.58 215.9 180.34 215.95 177.48 216.06V250.1C180.34 250.2 183.58 250.25 187.18 250.25H196.25Z"/>
|
||||
<path d="M324.59 213.35C318.34 213.35 312.61 215.03 307.41 218.37C302.22 221.7 297.93 225.65 294.54 230.22L276.09 217.97C282.13 210.65 289.36 204.66 297.79 200C306.22 195.33 315.47 193 325.55 193C332.33 193 338.53 193.87 344.15 195.62C349.77 197.37 354.59 199.84 358.63 203.02C362.65 206.2 365.78 210.07 368.01 214.63C370.23 219.19 371.35 224.27 371.35 229.89C371.35 235.2 370.36 239.86 368.4 243.89C366.44 247.92 363.85 251.6 360.61 254.94C357.38 258.28 353.69 261.38 349.56 264.25C345.42 267.11 341.18 269.97 336.84 272.84L317.27 285.72H370.87V306.55H279.42V286.04L318.39 260.27C322.2 257.73 325.86 255.32 329.36 253.03C332.86 250.76 335.93 248.42 338.58 246.04C341.24 243.65 343.35 241.13 344.95 238.48C346.54 235.83 347.33 232.91 347.33 229.74C347.33 227.41 346.72 225.23 345.5 223.22C344.28 221.2 342.64 219.45 340.57 217.97C338.51 216.49 336.09 215.35 333.34 214.55C330.58 213.75 327.66 213.35 324.59 213.35Z"/>
|
||||
<path d="M457.22 306.55V283.49H388.84V259.95L455.47 195.23H480.12V263.77H500V283.49H480.12V306.55L457.22 306.55ZM413.17 263.77H457.22V220.35L413.17 263.77Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
28
auth/assets/custom-icons/icons/bitazza.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="836.84424" height="914.80933">
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path stroke-linecap="round" d="M-186.41-125.66h372.816v351.267H-186.41Z"/>
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path stroke-linecap="round" d="M-186.41-95.22h372.816v351.267H-186.41Z"/>
|
||||
</clipPath>
|
||||
<clipPath id="c">
|
||||
<path stroke-linecap="round" d="M-160.08-140.83h372.816v351.267H-160.08Z"/>
|
||||
</clipPath>
|
||||
<clipPath id="d">
|
||||
<path stroke-linecap="round" d="M-212.74-140.83h372.816v351.267H-212.74Z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#a)" transform="translate(418.40805 457.4024)scale(3.64)">
|
||||
<path d="m298.246 129.84-6.486-4.585 6.486-4.586c12.26215-8.68661 19.24883-23.025924 18.53269-38.036075-.71613-15.010151-9.03624-28.619172-22.06969-36.098925l-.568-.349c-12.19994-7.033047-27.02434-7.819595-39.9-2.117l-7.053 3.144-.83-7.687c-1.51376-14.021838-9.62342-26.481265-21.832-33.542l-.2-.087C217.64616 2.0246361 210.0606-.00958995 202.34 0c-8.04957.00100343-15.94637 2.1978633-22.84 6.354l-1.114.677c-11.58345 7.090871-19.28369 19.094034-20.9 32.579l-.917 7.6-6.967-3.1c-12.8836-5.747507-27.74063-4.976398-39.96 2.074l-.7.394c-13.604075 7.890873-21.986349 22.420072-22.008 38.147v.787c.02516 14.283035 6.954274 27.67261 18.6 35.942L112 126.04l-6.463 4.586c-12.260313 8.67436-19.255033 22.99852-18.556462 38.00091.698572 15.00239 8.994121 28.61458 22.007462 36.11209l.633.393c12.22088 7.0137 27.04998 7.79342 39.939 2.1l7.054-3.123.807 7.687c1.51242 13.97659 9.57745 26.40246 21.727 33.475l.284.153c13.92748 8.0385 31.12909 7.84683 44.874-.5l1.092-.655c11.59433-7.07976 19.29833-19.08878 20.9-32.579l.9-7.6 6.987 3.123c12.9143 5.73028 27.78955 4.94317 40.027-2.118l.611-.372c13.60439-7.88085 21.98922-22.40383 22.012-38.126v-.808c-.0274-14.27871-6.94846-27.66532-18.583-35.943M292 166.591c-.0145 6.86067-3.67326 13.19693-9.608 16.639l-.654.394c-5.96032 3.4063-13.27768 3.4063-19.238 0l-17.884-10.351-22.841 13.211v20.133c-.0194 6.70609-3.51599 12.92214-9.237 16.421l-1.114.677c-5.99641 3.62641-13.48927 3.70989-19.565.218l-.24-.153c-5.94051-3.43919-9.60731-9.7748-9.63-16.639v-20.657l-22.841-13.211-17.884 10.351c-5.9447 3.40734-13.2503 3.40734-19.195 0l-.677-.415c-5.92257-3.40301-9.59053-9.69703-9.63171-16.52753-.0412-6.83049 3.5506-13.16829 9.43171-16.64247l18.648-11.027-.153-26.313-18.3-10.569c-5.94649-3.42296-9.61685-9.756715-9.63-16.618v-.787c.0184-6.865358 3.68634-13.20295 9.63-16.639l.655-.415c5.9528-3.406398 13.2642-3.406398 19.217 0l17.884 10.351 22.841-13.189V44.7c.0433-6.705114 3.54343-12.913272 9.258-16.421l1.114-.677c5.98556-3.637575 13.47843-3.721536 19.544-.219l.24.131c5.95616 3.434267 9.62726 9.785677 9.63 16.661v20.658l22.841 13.189L262.5 67.671c5.96032-3.406299 13.27768-3.406299 19.238 0l.654.415c5.91101 3.411809 9.56893 9.702792 9.61008 16.527659.0412 6.824866-3.54064 13.159502-9.41008 16.642341l-18.67 11.027.153 26.313 18.321 10.569c5.93573 3.43032 9.59519 9.76235 9.604 16.618z" style="opacity:1;fill:#10f48b;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" transform="translate(-201.88 -125.66)" vector-effect="non-scaling-stroke"/>
|
||||
</g>
|
||||
<g clip-path="url(#b)" transform="translate(418.40805 346.6008)scale(3.64)">
|
||||
<path d="m255.89 111.218-37.166 21.432-37.165-21.432 37.165-21.463Z" style="opacity:1;fill:#10f48b;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" transform="translate(-218.72 -111.2)" vector-effect="non-scaling-stroke"/>
|
||||
</g>
|
||||
<g clip-path="url(#c)" transform="translate(322.56685 512.6212)scale(3.64)">
|
||||
<path d="M209.3 153.613v42.927l-37.166-21.463v-42.9z" style="opacity:1;fill:#10f48b;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" transform="translate(-190.72 -164.36)" vector-effect="non-scaling-stroke"/>
|
||||
</g>
|
||||
<g clip-path="url(#d)" transform="translate(514.24925 512.6212)scale(3.64)">
|
||||
<path d="M273.357 132.181v42.9l-37.166 21.459v-42.927z" style="opacity:1;fill:#10f48b;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" transform="translate(-254.77 -164.36)" vector-effect="non-scaling-stroke"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1,29 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="logosandtypes_com" data-name="logosandtypes com" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #101010;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="186.97" y1="96.04" x2="45.7" y2="96.04" gradientTransform="translate(0 150.11) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#165cc3"/>
|
||||
<stop offset="1" stop-color="#3ddabb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_3" data-name="Layer 3">
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<path id="Layer_3-2" data-name="Layer 3-2" class="cls-2" d="M0,0H150V150H0V0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-1" d="M111.63,75.01c.06,.86,.08,1.72,.08,2.59,0,20.52-16.62,37.16-37.14,37.16-20.52,0-37.16-16.62-37.16-37.14,0-20.52,16.62-37.16,37.14-37.16,0,0,.02,0,.02,0,1.61,0,3.22,.1,4.82,.32l12.7-17.11C62.3,14,30.31,30.3,20.63,60.09c-9.68,29.79,6.62,61.78,36.41,71.47,29.79,9.68,61.78-6.62,71.47-36.41,4.29-13.2,3.59-27.52-1.97-40.24l-14.9,20.11Z"/>
|
||||
<polygon class="cls-3" points="120.26 4.82 74.49 66.53 62.93 53.99 45.67 69.89 76.4 103.32 149.5 4.82 120.26 4.82"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 150">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="186.97" x2="45.7" y1="96.04" y2="96.04"
|
||||
gradientTransform="matrix(1 0 0 -1 0 150.11)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#165cc3" />
|
||||
<stop offset="1" stop-color="#3ddabb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M111.63 75.01c.06.86.08 1.72.08 2.59 0 20.52-16.62 37.16-37.14 37.16S37.41 98.14 37.41 77.62s16.62-37.16 37.14-37.16h.02c1.61 0 3.22.1 4.82.32l12.7-17.11C62.3 14 30.31 30.3 20.63 60.09s6.62 61.78 36.41 71.47c29.79 9.68 61.78-6.62 71.47-36.41 4.29-13.2 3.59-27.52-1.97-40.24l-14.9 20.11Z"
|
||||
style="fill:#EFEFEF; mix-blend-mode: difference" />
|
||||
<path d="M120.26 4.82 74.49 66.53 62.93 53.99l-17.26 15.9 30.73 33.43 73.1-98.5z"
|
||||
style="fill:url(#a)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 876 B |
1
auth/assets/custom-icons/icons/cryptee.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="1400" height="1400" xmlns="http://www.w3.org/2000/svg"><path d="M699.914 0C1004.659 0 1263.915 194.786 1360 466.662l-399.246-.003C896.674 395.059 803.556 350 699.914 350c-193.276 0-349.957 156.7-349.957 350s156.681 350 349.957 350c103.641 0 196.76-45.059 260.84-116.658L1360 933.34C1263.915 1205.214 1004.659 1400 699.914 1400 313.362 1400 0 1086.6 0 700S313.362 0 699.914 0zm347.087 747.002L1398 747a696.274 696.274 0 0 1-12.453 93H1021a345.75 345.75 0 0 0 26.001-92.998zM1385.547 560A696.301 696.301 0 0 1 1398 653l-351-.002A345.762 345.762 0 0 0 1021 560h364.547z" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 608 B |
3
auth/assets/custom-icons/icons/finary.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="#F1C086" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M166.67 62C74.62 62 0 136.62 0 228.67H333.33C425.38 228.67 500 154.05 500 62H166.67ZM166.67 270.33C74.62 270.33 0 344.95 0 437H154.76C246.81 437 321.43 362.38 321.43 270.33H166.67Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 302 B |
@@ -1,15 +1,7 @@
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g style="mix-blend-mode:difference">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M363.636 23H409.091V477.545H363.636V23ZM0 23H45.4545V477.545H0V23ZM227.273 295.727H181.818V386.636H227.273V295.727ZM272.727 113.909H318.182V204.818H272.727V113.909Z" fill="white"/>
|
||||
</g>
|
||||
<path d="M136.364 386.636H45.4545V477.545H136.364V386.636Z" fill="#EA3326"/>
|
||||
<path d="M500 386.636H409.091V477.545H500V386.636Z" fill="#EA3326"/>
|
||||
<path d="M136.364 295.727H45.4545V386.636H136.364V295.727Z" fill="#EB5829"/>
|
||||
<path d="M318.182 295.727H227.273V386.636H318.182V295.727Z" fill="#EB5829"/>
|
||||
<path d="M500 295.727H409.091V386.636H500V295.727Z" fill="#EB5829"/>
|
||||
<path d="M136.364 23H45.4545V113.909H136.364V23Z" fill="#F7D046"/>
|
||||
<path d="M500 23H409.091V113.909H500V23Z" fill="#F7D046"/>
|
||||
<path d="M227.273 113.909H45.4545V204.818H227.273V113.909Z" fill="#F2A73B"/>
|
||||
<path d="M500 113.909H318.182V204.818H500V113.909Z" fill="#F2A73B"/>
|
||||
<path d="M500 204.818H45.4545V295.727H500V204.818Z" fill="#EE792F"/>
|
||||
</svg>
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M71.41 73H142.83V143.75H71.41V73ZM357.12 73H428.54V143.75H357.12V73Z" fill="#FFD800"/>
|
||||
<path d="M71.41 143.75H214.25V214.5H71.41V143.75ZM285.7 143.75H428.54V214.5H285.7V143.75Z" fill="#FFAF00"/>
|
||||
<path d="M71.41 214.5H428.54V285.25H71.41V214.5Z" fill="#FF8205"/>
|
||||
<path d="M71.41 285.27H142.83V356.02H71.41V285.27ZM214.27 285.27H285.69V356.02H214.27V285.27ZM357.12 285.27H428.54V356.02H357.12V285.27Z" fill="#FA500F"/>
|
||||
<path d="M0 356.06H214.3V426.82H0V356.06ZM285.7 356.06H500V426.82H285.7V356.06Z" fill="#E10500"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 628 B |
1
auth/assets/custom-icons/icons/oracle_cloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg height="2100" viewBox="0 0 32 21" width="3200" xmlns="http://www.w3.org/2000/svg"><path d="m9.9 20.1c-5.5 0-9.9-4.4-9.9-9.9s4.4-9.9 9.9-9.9h11.6c5.5 0 9.9 4.4 9.9 9.9s-4.4 9.9-9.9 9.9zm11.3-3.5c3.6 0 6.4-2.9 6.4-6.4 0-3.6-2.9-6.4-6.4-6.4h-11c-3.6 0-6.4 2.9-6.4 6.4s2.9 6.4 6.4 6.4z" fill="#c74634"/></svg>
|
||||
|
After Width: | Height: | Size: 310 B |
@@ -47,7 +47,7 @@
|
||||
"saveAction": "حفظ",
|
||||
"nextTotpTitle": "التالي",
|
||||
"deleteCodeTitle": "حذف الرمز؟",
|
||||
"deleteCodeMessage": "هل أنت متأكد من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.",
|
||||
"deleteCodeMessage": "هل أنت متيقِّن من أنك تريد حذف هذه الشيفرة؟ هذا الإجراء لا رجعة فيه.",
|
||||
"trashCode": "حذف الكود؟",
|
||||
"trashCodeMessage": "هل أنت متيقِّن أنك تريد حذف الكود الخاص بـ {account}؟",
|
||||
"trash": "سلة المهملات",
|
||||
@@ -513,5 +513,10 @@
|
||||
"free5GB": "5GB مجانًا على <bold-green>ente</bold-green> صور",
|
||||
"loginWithAuthAccount": "سجّل الدخول باستخدام حساب المُصادقة",
|
||||
"freeStorageOffer": "خَصْم 10٪ على صور <bold-green>ente</bold-green>",
|
||||
"freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى"
|
||||
"freeStorageOfferDescription": "استخدم الكود \"AUTH\" وأحصل على 10٪ خَصْم في السنة الأولى",
|
||||
"advanced": "متقدم",
|
||||
"algorithm": "الخوارزمية",
|
||||
"type": "النوع",
|
||||
"period": "المدّة",
|
||||
"digits": "الأرقام"
|
||||
}
|
||||
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Použít obnovovací klíč",
|
||||
"incorrectPasswordTitle": "Nesprávné heslo",
|
||||
"welcomeBack": "Vítejte zpět!",
|
||||
"emailAlreadyRegistered": "E-mail je již registrován.",
|
||||
"emailNotRegistered": "E-mail není registrován.",
|
||||
"madeWithLoveAtPrefix": "vyrobeno s ❤️ v ",
|
||||
"supportDevs": "Předplaťte si <bold-green>ente</bold-green>, abyste nás podpořili",
|
||||
"supportDiscount": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok",
|
||||
@@ -495,9 +497,18 @@
|
||||
"appLockOfflineModeWarning": "Zvolili jste si pokračování bez zálohování. Pokud zapomenete heslo do aplikace, přístup k datům bude uzamčen.",
|
||||
"duplicateCodes": "Duplikovat kódy",
|
||||
"noDuplicates": "✨ Žádné duplikáty",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Nemáte žádné duplicitní kódy k odstranění",
|
||||
"deduplicateCodes": "Deduplikovat kódy",
|
||||
"deselectAll": "Zrušit výběr všech položek",
|
||||
"selectAll": "Vybrat vše",
|
||||
"deleteDuplicates": "Odstranit duplikáty",
|
||||
"plainHTML": "Prosté HTML"
|
||||
"plainHTML": "Prosté HTML",
|
||||
"tellUsWhatYouThink": "Sdělte nám svůj názor",
|
||||
"dropReviewiOS": "Zanechat recenzi na App Store",
|
||||
"dropReviewAndroid": "Zanechat recenzi na Play Store",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
@@ -91,39 +91,39 @@
|
||||
"emailAlreadyRegistered": "E-Mail ist bereits registriert.",
|
||||
"emailNotRegistered": "E-Mail-Adresse nicht registriert.",
|
||||
"madeWithLoveAtPrefix": "gemacht mit ❤️ bei ",
|
||||
"supportDevs": "Bei <bold-green>ente</bold-green> registrieren, um das Projekt zu unterstützen",
|
||||
"supportDevs": "Abonnieren Sie <bold-green>ente</bold-green>, um das Projekt zu unterstützen",
|
||||
"supportDiscount": "Benutzen Sie den Rabattcode \"AUTH\" für 10 % Rabatt im ersten Jahr",
|
||||
"changeEmail": "E-Mail ändern",
|
||||
"changePassword": "Passwort ändern",
|
||||
"data": "Datei",
|
||||
"data": "Daten",
|
||||
"importCodes": "Codes importieren",
|
||||
"importTypePlainText": "Klartext",
|
||||
"importTypeEnteEncrypted": "Verschlüsselter Ente-Export",
|
||||
"passwordForDecryptingExport": "Passwort um den Export zu entschlüsseln",
|
||||
"passwordEmptyError": "Passwort kann nicht leer sein",
|
||||
"importFromApp": "Importiere Codes von {appName}",
|
||||
"importGoogleAuthGuide": "Exportiere deine Accounts von Google Authenticator zu einem QR-Code, durch die \"Konten übertragen\" Option. Scanne den QR-Code danach mit einem anderen Gerät.\n\nTipp: Du kannst die Kamera eines Laptops verwenden, um ein Foto den dem QR-Code zu erstellen.",
|
||||
"importSelectJsonFile": "Wähle eine JSON-Datei",
|
||||
"importFromApp": "Importieren Sie Codes von {appName}",
|
||||
"importGoogleAuthGuide": "Exportieren Sie Ihre Accounts von Google Authenticator zu einem QR-Code, mithilfe der \"Konten übertragen\" Option. Scanne den QR-Code danach mit einem anderen Gerät.\n\nTipp: Sie können die Kamera eines Laptops verwenden, um ein Foto vom QR-Code zu erstellen.",
|
||||
"importSelectJsonFile": "Wählen Sie eine JSON-Datei",
|
||||
"importSelectAppExport": "{appName} Exportdatei auswählen",
|
||||
"importEnteEncGuide": "Wähle die von Ente exportierte, verschlüsselte JSON-Datei",
|
||||
"importRaivoGuide": "Verwenden Sie die Option \"Export OTPs to Zip archive\" in den Raivo-Einstellungen.\n\nEntpacken Sie die Zip-Datei und importieren Sie die JSON-Datei.",
|
||||
"importEnteEncGuide": "Wählen Sie die von Ente exportierte, verschlüsselte JSON-Datei",
|
||||
"importRaivoGuide": "Verwenden Sie die Option \"Export OTPs to Zip archive\" in den Einstellungen von Raivo.\n\nEntpacken Sie die Zip-Datei und importieren Sie die JSON-Datei.",
|
||||
"importBitwardenGuide": "Verwenden Sie die Option \"Tresor exportieren\" innerhalb der Bitwarden Tools und importieren Sie die unverschlüsselte JSON-Datei.",
|
||||
"importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Aegis-Einstellungen.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.",
|
||||
"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.",
|
||||
"exportCodes": "Codes exportieren",
|
||||
"importLabel": "Importieren",
|
||||
"importInstruction": "Bitte wählen sie eine Datei die Codes in folgendem Format beinhaltet",
|
||||
"importCodeDelimiterInfo": "Codes können in einer neuen Zeile stehen oder durch Kommata getrennt sein",
|
||||
"importInstruction": "Bitte wählen Sie eine Datei die Codes in folgendem Format beinhaltet",
|
||||
"importCodeDelimiterInfo": "Codes können in einer neuen Zeile stehen oder durch ein Komma getrennt sein",
|
||||
"selectFile": "Datei auswählen",
|
||||
"emailVerificationToggle": "E-Mail-Verifizierung",
|
||||
"emailVerificationEnableWarning": "Stellen Sie sicher, eine Kopie Ihrer Zwei-Faktor-Authentifizierung an anderer Stelle zu speichern, um zu vermeiden, dass Sie sich versehentlich aus Ihrem Account aussperren.",
|
||||
"authToChangeEmailVerificationSetting": "Bitte Authentifizieren um die E-Mail Bestätigung zu ändern",
|
||||
"emailVerificationEnableWarning": "Um zu vermeiden, versehentlich aus Ihrem Konto ausgesperrt zu werden, stellen Sie sicher, dass Sie den Zwei-Faktor-Authentifizierungscode für Ihr E-Mail-Konto außerhalb von Ente Auth speichern, bevor Sie die E-Mail-Verifizierung aktivieren.",
|
||||
"authToChangeEmailVerificationSetting": "Bitte authentifizieren, um die E-Mail Bestätigung zu ändern",
|
||||
"authenticateGeneric": "Bitte authentifizieren",
|
||||
"authToViewYourRecoveryKey": "Bitte authentifizieren um ihren Wiederherstellungscode anzuzeigen",
|
||||
"authToChangeYourEmail": "Bitte authentifizieren um ihre Emailadresse zu ändern",
|
||||
"authToChangeYourPassword": "Bitte authentifizieren um ihr Passwort zu ändern",
|
||||
"authToViewSecrets": "Bitte authentifizieren Sie sich, um ihren Wiederherstellungscode anzuzeigen",
|
||||
"authToViewYourRecoveryKey": "Bitte authentifizieren, um Ihren Wiederherstellungscode anzuzeigen",
|
||||
"authToChangeYourEmail": "Bitte authentifizieren, um Ihre E-Mail-Adresse zu ändern",
|
||||
"authToChangeYourPassword": "Bitte authentifizieren, um Ihr Passwort zu ändern",
|
||||
"authToViewSecrets": "Bitte authentifizieren, um Ihren Wiederherstellungscode anzuzeigen",
|
||||
"authToInitiateSignIn": "Bitte authentifizieren, um die Anmeldung zum Backup zu starten.",
|
||||
"ok": "Ok",
|
||||
"cancel": "Abbrechen",
|
||||
@@ -140,18 +140,18 @@
|
||||
"delete": "Löschen",
|
||||
"enterYourPasswordHint": "Geben Sie Ihr Passwort ein",
|
||||
"forgotPassword": "Passwort vergessen",
|
||||
"oops": "Hopla",
|
||||
"oops": "Hoppla",
|
||||
"suggestFeatures": "Features vorschlagen",
|
||||
"faq": "FAQ",
|
||||
"somethingWentWrongMessage": "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut",
|
||||
"leaveFamily": "Familie verlassen",
|
||||
"leaveFamilyMessage": "Sind Sie sicher, dass Sie den Familien-Plan verlassen wollen?",
|
||||
"inFamilyPlanMessage": "Sie haben einen Familien-Plan!",
|
||||
"hintForMobile": "Lange drücken, um den Code zu bearbeiten oder zu entfernen.",
|
||||
"hintForMobile": "Lange auf einen Code drücken, um ihn zu bearbeiten oder zu entfernen.",
|
||||
"hintForDesktop": "Klicken Sie mit der rechten Maustaste auf einen Code zum Bearbeiten oder Entfernen.",
|
||||
"scan": "Scannen",
|
||||
"scanACode": "Scan einen Code",
|
||||
"verify": "Überprüfen Sie",
|
||||
"verify": "Verifizieren",
|
||||
"verifyEmail": "E-Mail-Adresse verifizieren",
|
||||
"enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
|
||||
"lostDeviceTitle": "Gerät verloren?",
|
||||
@@ -172,9 +172,10 @@
|
||||
},
|
||||
"invalidQRCode": "Ungültiger QR-Code",
|
||||
"noRecoveryKeyTitle": "Kein Wiederherstellungsschlüssel?",
|
||||
"enterEmailHint": "Geben Sie Ihre E-Mail Adresse ein",
|
||||
"invalidEmailTitle": "Ungültige E-Mail Adresse",
|
||||
"invalidEmailMessage": "Bitte geben Sie eine gültige E-Mail Adresse ein.",
|
||||
"enterEmailHint": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
"enterNewEmailHint": "Gib deine neue E-Mail-Adresse ein",
|
||||
"invalidEmailTitle": "Ungültige E-Mail-Adresse",
|
||||
"invalidEmailMessage": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
|
||||
"deleteAccount": "Konto löschen",
|
||||
"deleteAccountQuery": "Es tut uns leid, dass Sie gehen. Haben Sie ein Problem?",
|
||||
"yesSendFeedbackAction": "Ja, Feedback senden",
|
||||
@@ -187,7 +188,7 @@
|
||||
"moderateStrength": "Mittel",
|
||||
"confirmPassword": "Bestätigen Sie das Passwort",
|
||||
"close": "Schließen",
|
||||
"oopsSomethingWentWrong": "Ups, da ist etwas schief gelaufen.",
|
||||
"oopsSomethingWentWrong": "Hoppla, da ist etwas schiefgelaufen.",
|
||||
"selectLanguage": "Sprache auswählen",
|
||||
"language": "Sprache",
|
||||
"social": "Social",
|
||||
@@ -203,26 +204,26 @@
|
||||
"noResult": "Kein Ergebnis",
|
||||
"addCode": "Code hinzufügen",
|
||||
"scanAQrCode": "QR-Code scannen",
|
||||
"enterDetailsManually": "Details manuell hinzufügen",
|
||||
"enterDetailsManually": "Daten manuell hinzufügen",
|
||||
"edit": "Editieren",
|
||||
"share": "Teilen",
|
||||
"shareCodes": "Codes teilen",
|
||||
"shareCodesDuration": "Wählen Sie die Dauer aus, für die Sie die Codes teilen möchten.",
|
||||
"restore": "Wiederherstellen",
|
||||
"copiedToClipboard": "In die Zwischenablage kopieren",
|
||||
"copiedNextToClipboard": "Nächster Code wurde in die Zwischenablage kopiert",
|
||||
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||
"copiedNextToClipboard": "Nächster Code in die Zwischenablage kopiert",
|
||||
"error": "Fehler",
|
||||
"recoveryKeyCopiedToClipboard": "Wiederherstellungsschlüssel in die Zwischenablage kopiert",
|
||||
"recoveryKeyOnForgotPassword": "Sollten sie ihr Passwort vergessen, dann ist dieser Schlüssel die einzige Möglichkeit ihre Daten wiederherzustellen.",
|
||||
"recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern sie dieses diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.",
|
||||
"recoveryKeySaveDescription": "Wir speichern diesen Schlüssel nicht. Sichern Sie diesen Schlüssel bestehend aus 24 Wörtern an einem sicheren Platz.",
|
||||
"doThisLater": "Auf später verschieben",
|
||||
"saveKey": "Schlüssel speichern",
|
||||
"save": "Speichern",
|
||||
"send": "Senden",
|
||||
"saveOrSendDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) oder an andere Apps senden?",
|
||||
"saveOrSendDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) speichern oder an andere Apps senden?",
|
||||
"saveOnlyDescription": "Möchtest du dies in deinem Speicher (standardmäßig im Ordner Downloads) speichern?",
|
||||
"back": "Zurück",
|
||||
"createAccount": "Account erstellen",
|
||||
"createAccount": "Konto erstellen",
|
||||
"passwordStrength": "Passwortstärke: {passwordStrengthValue}",
|
||||
"@passwordStrength": {
|
||||
"description": "Text to indicate the password strength",
|
||||
@@ -244,17 +245,17 @@
|
||||
"changePasswordTitle": "Passwort ändern",
|
||||
"resetPasswordTitle": "Passwort zurücksetzen",
|
||||
"encryptionKeys": "Verschlüsselungsschlüssel",
|
||||
"passwordWarning": "Wir speichern dieses Passwort nicht. Wenn du es vergisst, <underline>können wir deine Daten nicht entschlüsseln</underline>",
|
||||
"enterPasswordToEncrypt": "Gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können",
|
||||
"enterNewPasswordToEncrypt": "Gib ein neues Passwort ein, mit dem wir deine Daten verschlüsseln können",
|
||||
"passwordWarning": "Wir speichern dieses Passwort nicht. Wenn Sie es vergessen, <underline>können wir Ihre Daten nicht entschlüsseln</underline>",
|
||||
"enterPasswordToEncrypt": "Geben Sie ein Passwort ein, mit dem wir Ihre Daten verschlüsseln können",
|
||||
"enterNewPasswordToEncrypt": "Geben Sie ein neues Passwort ein, mit dem wir Ihre Daten verschlüsseln können",
|
||||
"passwordChangedSuccessfully": "Passwort erfolgreich geändert",
|
||||
"generatingEncryptionKeys": "Generierung von Verschlüsselungsschlüsseln...",
|
||||
"continueLabel": "Weiter",
|
||||
"insecureDevice": "Unsicheres Gerät",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Es tut uns leid, wir konnten keine sicheren Schlüssel auf diesem Gerät generieren.\n\nBitte registrieren Sie sich auf einem anderen Gerät.",
|
||||
"howItWorks": "So funktioniert's",
|
||||
"ackPasswordLostWarning": "Ich verstehe, dass der Verlust meines Passworts zum Verlust meiner Daten führen kann, denn diese ist <underline>Ende-zu-Ende verschlüsselt</underline>.",
|
||||
"loginTerms": "Durch das Klicken auf den Login-Button, stimme ich <u-terms>den Nutzungsbedingungen</u-terms> und den <u-policy>Datenschutzbestimmungen</u-policy> zu",
|
||||
"ackPasswordLostWarning": "Ich verstehe, dass der Verlust meines Passworts zum Verlust meiner Daten führen kann, denn diese sind <underline>Ende-zu-Ende verschlüsselt</underline>.",
|
||||
"loginTerms": "Durch das Klicken auf den Login-Button, stimme ich den <u-terms> Nutzungsbedingungen</u-terms> und den <u-policy>Datenschutzbestimmungen</u-policy> zu",
|
||||
"logInLabel": "Einloggen",
|
||||
"logout": "Ausloggen",
|
||||
"areYouSureYouWantToLogout": "Sind sie sicher, dass sie sich ausloggen möchten?",
|
||||
@@ -266,7 +267,7 @@
|
||||
"systemTheme": "System",
|
||||
"verifyingRecoveryKey": "Verifiziere Wiederherstellungsschlüssel...",
|
||||
"recoveryKeyVerified": "Wiederherstellungsschlüssel verifiziert",
|
||||
"recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken sie daran, dass sie ihren Wiederherstellungsschlüssel sicher aufbewahren.",
|
||||
"recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken Sie daran, den Wiederherstellungsschlüssel sicher aufzubewahren.",
|
||||
"invalidRecoveryKey": "Der eingegebene Wiederherstellungsschlüssel ist nicht gültig. Bitte stellen sie sicher, dass er aus 24 Wörtern besteht und prüfen sie die Schreibweise eines jeden.\n\nSollten sie einen Wiederherstellungsschlüssel im alten Format eingegeben haben vergewissern sie sich, dass er 64 Zeichen lang ist und prüfen sie jedes dieser Zeichen.",
|
||||
"recreatePasswordTitle": "Neues Passwort erstellen",
|
||||
"recreatePasswordBody": "Das benutzte Gerät ist nicht leistungsfähig genug das Passwort zu prüfen. Wir können es aber neu erstellen damit es auf jedem Gerät funktioniert. \n\nBitte loggen sie sich mit ihrem Wiederherstellungsschlüssel ein und erstellen sie ein neues Passwort (Sie können das selbe Passwort wieder verwenden, wenn sie möchten).",
|
||||
@@ -277,15 +278,15 @@
|
||||
"recoveryKeyVerifyReason": "Ihr Wiederherstellungsschlüssel ist der einzige Weg ihre Fotos wiederherzustellen sollten, sie ihr Passwort vergessen. Sie finden ihren Wiederherstellungsschlüssel unter Einstellungen > Account.\n\nBitte tragen sie ihren Wiederherstellungsschlüssel hier ein um zu prüfen ob sie in korrekt abgespeichert haben.",
|
||||
"confirmYourRecoveryKey": "Wiederherstellungsschlüssel bestätigen",
|
||||
"confirm": "Bestätigen",
|
||||
"emailYourLogs": "Email mit Logs senden",
|
||||
"emailYourLogs": "E-Mail mit Logs senden",
|
||||
"pleaseSendTheLogsTo": "Bitte Logs an {toEmail} senden",
|
||||
"copyEmailAddress": "Emailadresse kopieren",
|
||||
"copyEmailAddress": "E-Mail-Adresse kopieren",
|
||||
"exportLogs": "Logs exportieren",
|
||||
"enterYourRecoveryKey": "Wiederherstellungsschlüssel eingeben",
|
||||
"tempErrorContactSupportIfPersists": "Etwas ist schiefgelaufen. Bitte versuchen sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.",
|
||||
"tempErrorContactSupportIfPersists": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.",
|
||||
"networkHostLookUpErr": "Ente ist im Moment nicht erreichbar. Bitte überprüfen Sie Ihre Netzwerkeinstellungen. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.",
|
||||
"networkConnectionRefusedErr": "Ente ist im Moment nicht erreichbar. Bitte versuchen Sie es später erneut. Sollte das Problem bestehen bleiben, wenden Sie sich bitte an den Support.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuchen sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren sie unser Supportteam.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuchen Sie es später noch einmal. Sollte der Fehler weiter bestehen, kontaktieren Sie unser Supportteam.",
|
||||
"about": "Über uns",
|
||||
"weAreOpenSource": "Wir sind Opensource!",
|
||||
"privacy": "Datenschutz",
|
||||
@@ -316,10 +317,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sorry": "Entschuldigen sie",
|
||||
"importFailureDesc": "Ausgewählte Datei ließ sich nicht verarbeiten.\nBitte wenden sie sich an support@ente.io für Hilfe!",
|
||||
"sorry": "Entschuldigen Sie",
|
||||
"importFailureDesc": "Ausgewählte Datei ließ sich nicht verarbeiten.\nBitte wenden Sie sich an support@ente.io für Hilfe!",
|
||||
"pendingSyncs": "Warnung",
|
||||
"pendingSyncsWarningBody": "Einige Codes wurden nicht gesichert.\n\nBitte gehen sie sicher, dass sie einen Backupcode für diese Codes haben bevor sie sich ausloggen.",
|
||||
"pendingSyncsWarningBody": "Einige Codes wurden nicht gesichert.\n\nBitte gehen Sie sicher, dass Sie einen Backupcode für diese Codes haben bevor Sie sich ausloggen.",
|
||||
"checkInboxAndSpamFolder": "Bitte überprüfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen",
|
||||
"tapToEnterCode": "Antippen, um den Code einzugeben",
|
||||
"resendEmail": "E-Mail erneut senden",
|
||||
@@ -339,10 +340,10 @@
|
||||
"mostFrequentlyUsed": "Häufig verwendet",
|
||||
"mostRecentlyUsed": "Zuletzt verwendet",
|
||||
"activeSessions": "Aktive Sitzungen",
|
||||
"somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte versuche es erneut",
|
||||
"thisWillLogYouOutOfThisDevice": "Dadurch wirst du von diesem Gerät abgemeldet!",
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Dadurch wirst du von folgendem Gerät abgemeldet:",
|
||||
"terminateSession": "Sitzungen beenden?",
|
||||
"somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte erneut versuchen",
|
||||
"thisWillLogYouOutOfThisDevice": "Dadurch werden Sie von diesem Gerät abgemeldet!",
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Dadurch werden Sie vom folgendem Gerät abgemeldet:",
|
||||
"terminateSession": "Sitzung beenden?",
|
||||
"terminate": "Beenden",
|
||||
"thisDevice": "Dieses Gerät",
|
||||
"toResetVerifyEmail": "Um Ihr Passwort zurückzusetzen, verifizieren Sie bitte zuerst Ihre E-Mail-Adresse.",
|
||||
@@ -352,7 +353,7 @@
|
||||
"incorrectCode": "Falscher Code",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Leider ist der eingegebene Code falsch",
|
||||
"emailChangedTo": "E-Mail-Adresse geändert zu {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "Authentifizierung fehlgeschlagen, versuchen Sie es bitte erneut",
|
||||
"authenticationFailedPleaseTryAgain": "Authentifizierung fehlgeschlagen, bitte erneut versuchen",
|
||||
"authenticationSuccessful": "Authentifizierung erfolgreich!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Zwei-Faktor-Authentifizierung (2FA) erfolgreich zurückgesetzt",
|
||||
"incorrectRecoveryKey": "Falscher Wiederherstellungs-Schlüssel",
|
||||
@@ -365,22 +366,22 @@
|
||||
"passwordToEncryptExport": "Passwort zum Verschlüssen des Exports",
|
||||
"export": "Export",
|
||||
"useOffline": "Ohne Backup verwenden",
|
||||
"signInToBackup": "Melde dich an, um deine Codes zu sichern",
|
||||
"signInToBackup": "Melden Sie sich an, um Ihre Codes zu sichern",
|
||||
"singIn": "Anmelden",
|
||||
"sigInBackupReminder": "Bitte exportieren Sie Ihre Codes, um sicherzustellen, dass Sie ein Backup haben, aus dem Sie wiederherstellen können.",
|
||||
"sigInBackupReminder": "Bitte exportieren Sie Ihre Codes, um sicherzustellen, dass Sie ein Backup haben, das Sie wiederherstellen können.",
|
||||
"offlineModeWarning": "Sie haben sich dafür entschieden, ohne Sicherungen fortzufahren. Bitte führen Sie manuelle Sicherungen durch, um sicherzustellen, dass Ihre Codes sicher sind.",
|
||||
"showLargeIcons": "Große Symbole anzeigen",
|
||||
"compactMode": "Kompaktmodus",
|
||||
"shouldHideCode": "Codes ausblenden",
|
||||
"doubleTapToViewHiddenCode": "Sie können auf einen Eintrag doppelt tippen, um den Code anzuzeigen",
|
||||
"focusOnSearchBar": "Suche bei App-Start automatisch öffnen",
|
||||
"confirmUpdatingkey": "Sind Sie sich sicher, dass Sie den Secret Key bearbeiten wollen?",
|
||||
"focusOnSearchBar": "Suche beim App-Start fokussieren",
|
||||
"confirmUpdatingkey": "Sind Sie sich sicher, dass Sie den geheimen Schlüssel bearbeiten wollen?",
|
||||
"minimizeAppOnCopy": "Beim Kopieren App minimieren",
|
||||
"editCodeAuthMessage": "Authentifizieren, um Code zu bearbeiten",
|
||||
"deleteCodeAuthMessage": "Authentifizieren, um Code zu löschen",
|
||||
"showQRAuthMessage": "Authentifizieren, um QR-Code anzuzeigen",
|
||||
"confirmAccountDeleteTitle": "Kontolöschung bestätigen",
|
||||
"confirmAccountDeleteMessage": "Dieses Konto ist mit anderen Ente-Apps verknüpft, falls du welche verwendest.\n\nDeine hochgeladenen Daten werden in allen Ente-Apps zur Löschung vorgemerkt und dein Konto wird endgültig gelöscht.",
|
||||
"confirmAccountDeleteMessage": "Dieses Konto ist mit anderen Ente-Apps verknüpft, falls Sie welche verwenden.\n\nIhre hochgeladenen Daten werden in allen Ente-Apps zur Löschung vorgemerkt und Ihr Konto wird endgültig gelöscht.",
|
||||
"androidBiometricHint": "Identität bestätigen",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
@@ -417,7 +418,7 @@
|
||||
"@goToSettings": {
|
||||
"description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
|
||||
},
|
||||
"androidGoToSettingsDescription": "Auf Ihrem Gerät ist keine biometrische Authentifizierung eingerichtet. Gehen Sie „Einstellungen“ > „Sicherheit“, um die biometrische Authentifizierung hinzuzufügen.",
|
||||
"androidGoToSettingsDescription": "Auf Ihrem Gerät ist keine biometrische Authentifizierung eingerichtet. Gehen Sie zu 'Einstellungen > Sicherheit', um die biometrische Authentifizierung hinzuzufügen.",
|
||||
"@androidGoToSettingsDescription": {
|
||||
"description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side."
|
||||
},
|
||||
@@ -434,9 +435,9 @@
|
||||
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
|
||||
},
|
||||
"noInternetConnection": "Keine Internetverbindung",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Bitte überprüfe deine Internetverbindung und versuche es erneut.",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "Bitte überprüfen Sie Ihre Internetverbindung und versuchen Sie erneut.",
|
||||
"signOutFromOtherDevices": "Von anderen Geräten abmelden",
|
||||
"signOutOtherBody": "Falls du denkst, dass jemand dein Passwort kennen könnte, kannst du alle anderen Geräte von deinem Account abmelden.",
|
||||
"signOutOtherBody": "Falls Sie denken, dass jemand Ihr Passwort kennen könnte, können Sie alle anderen Geräte forcieren, sich von Ihrem Konto abzumelden.",
|
||||
"signOutOtherDevices": "Andere Geräte abmelden",
|
||||
"doNotSignOut": "Nicht abmelden",
|
||||
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
|
||||
@@ -458,7 +459,7 @@
|
||||
"pinText": "Anpinnen",
|
||||
"unpinText": "Lösen",
|
||||
"pinnedCodeMessage": "{code} wurde angepinnt",
|
||||
"unpinnedCodeMessage": "{code} wird nicht weiter angepinnt",
|
||||
"unpinnedCodeMessage": "{code} wurde losgelöst",
|
||||
"pinned": "Angeheftet",
|
||||
"tags": "Tags",
|
||||
"createNewTag": "Neuen Tag erstellen",
|
||||
@@ -474,7 +475,7 @@
|
||||
"rawCodeData": "Rohcode Daten",
|
||||
"appLock": "App-Sperre",
|
||||
"noSystemLockFound": "Keine Systemsperre gefunden",
|
||||
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Um die App-Sperre zu aktivieren, konfiguriere bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen.",
|
||||
"toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Um die App-Sperre zu aktivieren, konfigurieren Sie bitte den Gerätepasscode oder die Bildschirmsperre in den Systemeinstellungen.",
|
||||
"autoLock": "Automatisches Sperren",
|
||||
"immediately": "Sofort",
|
||||
"reEnterPassword": "Passwort erneut eingeben",
|
||||
@@ -494,23 +495,29 @@
|
||||
"setNewPin": "Neue PIN festlegen",
|
||||
"importFailureDescNew": "Die ausgewählte Datei konnte nicht verarbeitet werden.",
|
||||
"appLockNotEnabled": "App-Sperre nicht aktiviert",
|
||||
"appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre",
|
||||
"authToViewPasskey": "Bitte authentifizieren, um deinen Passkey zu sehen",
|
||||
"duplicateCodes": "Doppelte Codes",
|
||||
"appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Sicherheit > App-Sperre",
|
||||
"authToViewPasskey": "Bitte authentifizieren, um Ihren Passkey zu sehen",
|
||||
"appLockOfflineModeWarning": "Sie haben sich dazu entschieden, ohne Sicherungen fortzufahren. Wenn Sie Ihre App-Sperre vergessen, können Sie nicht mehr auf Ihre Daten zugreifen.",
|
||||
"duplicateCodes": "Codes duplizieren",
|
||||
"noDuplicates": "✨ Keine Duplikate",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Du hast keine doppelten Codes, die bereinigt werden können",
|
||||
"deduplicateCodes": "Codes deduplizieren",
|
||||
"deselectAll": "Alle abwählen",
|
||||
"selectAll": "Alles auswählen",
|
||||
"selectAll": "Alle auswählen",
|
||||
"deleteDuplicates": "Duplikate löschen",
|
||||
"plainHTML": "Reines HTML",
|
||||
"tellUsWhatYouThink": "Sagen Sie uns, was Sie denken",
|
||||
"dropReviewiOS": "Hinterlasse eine Rezension im App Store",
|
||||
"dropReviewAndroid": "Hinterlasse eine Rezension im Google Play Store",
|
||||
"supportEnte": "Support <bold-green>ente</bold-green>",
|
||||
"dropReviewiOS": "Hinterlassen Sie eine Rezension im App Store",
|
||||
"dropReviewAndroid": "Hinterlassen Sie eine Rezension im Google Play Store",
|
||||
"supportEnte": "Unterstütze <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Gib uns einen Stern auf Github",
|
||||
"free5GB": "5GB kostenlos auf <bold-green>ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Mit Ihrem Auth Account anmelden",
|
||||
"freeStorageOffer": "10% Rabatt für <bold-green>ente</bold-green> Photos",
|
||||
"freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im 1. Jahr zu sparen"
|
||||
"freeStorageOfferDescription": "Verwende den Code \"AUTH\", um 10% im ersten Jahr zu sparen",
|
||||
"advanced": "Erweitert",
|
||||
"algorithm": "Algorithmus",
|
||||
"type": "Typ",
|
||||
"period": "Periode",
|
||||
"digits": "Ziffern"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Invalid QR code",
|
||||
"noRecoveryKeyTitle": "No recovery key?",
|
||||
"enterEmailHint": "Enter your email address",
|
||||
"enterNewEmailHint": "Enter your new email address",
|
||||
"invalidEmailTitle": "Invalid email address",
|
||||
"invalidEmailMessage": "Please enter a valid email address.",
|
||||
"deleteAccount": "Delete account",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5GB free on <bold-green>ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Login with your Auth account",
|
||||
"freeStorageOffer": "10% off on <bold-green>ente</bold-green> photos",
|
||||
"freeStorageOfferDescription": "Use code \"AUTH\" to get 10% off first year"
|
||||
"freeStorageOfferDescription": "Use code \"AUTH\" to get 10% off first year",
|
||||
"advanced": "Advanced",
|
||||
"algorithm": "Algorithm",
|
||||
"type": "Type",
|
||||
"period": "Period",
|
||||
"digits": "Digits"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "QR code non valide",
|
||||
"noRecoveryKeyTitle": "Pas de clé de récupération ?",
|
||||
"enterEmailHint": "Entrez votre adresse e-mail",
|
||||
"enterNewEmailHint": "Saisissez votre nouvelle adresse email",
|
||||
"invalidEmailTitle": "Adresse e-mail invalide",
|
||||
"invalidEmailMessage": "Veuillez saisir une adresse e-mail valide.",
|
||||
"deleteAccount": "Supprimer le compte",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5 Go gratuits sur <bold-green>Ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Connectez-vous avec votre compte Auth",
|
||||
"freeStorageOffer": "10% de réduction sur <bold-green>Ente</bold-green> Photos",
|
||||
"freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année"
|
||||
"freeStorageOfferDescription": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction la première année",
|
||||
"advanced": "Avancé",
|
||||
"algorithm": "Algorithme",
|
||||
"type": "Type",
|
||||
"period": "Période",
|
||||
"digits": "Chiffres"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Érvénytelen QR-kód",
|
||||
"noRecoveryKeyTitle": "Nincs helyreállítási kulcs?",
|
||||
"enterEmailHint": "Adja meg az e-mail címét",
|
||||
"enterNewEmailHint": "Add meg az új e-mail címed",
|
||||
"invalidEmailTitle": "Érvénytelen e-mail cím",
|
||||
"invalidEmailMessage": "Kérjük, adjon meg egy érvényes e-mail címet.",
|
||||
"deleteAccount": "Fiók törlése",
|
||||
@@ -513,5 +514,6 @@
|
||||
"free5GB": "5GB ingyen <bold-green>ente <bold-green> Photos",
|
||||
"loginWithAuthAccount": "Jelentkezzen be Auth fiókjával",
|
||||
"freeStorageOffer": "10% kedvezmény on <bold-green>ente<bold-green> photos",
|
||||
"freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben"
|
||||
"freeStorageOfferDescription": "Használja az \"AUTH\" kódot, hogy 10% kedvezményt kapjon az első évben",
|
||||
"type": "Típus"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Codice QR non valido",
|
||||
"noRecoveryKeyTitle": "Nessuna chiave di recupero?",
|
||||
"enterEmailHint": "Inserisci il tuo indirizzo email",
|
||||
"enterNewEmailHint": "Inserisci il tuo nuovo indirizzo email",
|
||||
"invalidEmailTitle": "Indirizzo email non valido",
|
||||
"invalidEmailMessage": "Inserisci un indirizzo email valido.",
|
||||
"deleteAccount": "Elimina account",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5GB gratis su <bold-green>ente</bold-green> Foto",
|
||||
"loginWithAuthAccount": "Accedi con il tuo account Auth",
|
||||
"freeStorageOffer": "10% di sconto su <bold-green>ente</bold-green> Foto",
|
||||
"freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno"
|
||||
"freeStorageOfferDescription": "Utilizzare il codice \"AUTH\" per ottenere il 10% di sconto al primo anno",
|
||||
"advanced": "Avanzate",
|
||||
"algorithm": "Algoritmo",
|
||||
"type": "Tipo",
|
||||
"period": "Periodo",
|
||||
"digits": "Cifre"
|
||||
}
|
||||
@@ -508,9 +508,15 @@
|
||||
"tellUsWhatYouThink": "Pasakykite mums, ką manote",
|
||||
"dropReviewiOS": "Rašyti apžvalgą parduotuvėje „App Store“",
|
||||
"dropReviewAndroid": "Rašyti apžvalgą parduotuvėje „Play“ parduotuvė“",
|
||||
"supportEnte": "Paremti „<bold-green>ente</bold-green>“",
|
||||
"giveUsAStarOnGithub": "Suteikite mums žvaigždutę platformoje „Github“",
|
||||
"free5GB": "5 GB nemokami programai „<bold-green>ente</bold-green>“ nuotraukos",
|
||||
"loginWithAuthAccount": "Prisijungti su jūsų „Auth“ paskyra",
|
||||
"freeStorageOffer": "10 % nuolaida programai „<bold-green>ente</bold-green>“ nuotraukos",
|
||||
"freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. "
|
||||
"freeStorageOfferDescription": "Naudokite kodą „AUTH“, kad gautumėte 10 % nuolaida pirmiesiems metams. ",
|
||||
"advanced": "Išplėstiniai",
|
||||
"algorithm": "Algoritmas",
|
||||
"type": "Tipas",
|
||||
"period": "Laikotarpis",
|
||||
"digits": "Skaitmenys"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Ongeldige QR-code",
|
||||
"noRecoveryKeyTitle": "Geen herstelsleutel?",
|
||||
"enterEmailHint": "Voer je e-mailadres in",
|
||||
"enterNewEmailHint": "Voer uw nieuwe e-mailadres in",
|
||||
"invalidEmailTitle": "Ongeldig e-mailadres",
|
||||
"invalidEmailMessage": "Voer een geldig e-mailadres in.",
|
||||
"deleteAccount": "Account verwijderen",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5GB gratis op <bold-green>ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Log in met je Auth account",
|
||||
"freeStorageOffer": "10% korting op <bold-green>ente</bold-green> photos",
|
||||
"freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar"
|
||||
"freeStorageOfferDescription": "Gebruik de code \"AUTH\" voor 10% korting op je eerste jaar",
|
||||
"advanced": "Geavanceerd",
|
||||
"algorithm": "Algoritme",
|
||||
"type": "Type",
|
||||
"period": "Periode",
|
||||
"digits": "Cijfers"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Nieprawidłowy kod QR",
|
||||
"noRecoveryKeyTitle": "Brak klucza odzyskiwania?",
|
||||
"enterEmailHint": "Wprowadź adres e-mail",
|
||||
"enterNewEmailHint": "Wprowadź nowy adres e-mail",
|
||||
"invalidEmailTitle": "Nieprawidłowy adres e-mail",
|
||||
"invalidEmailMessage": "Prosimy podać prawidłowy adres e-mail.",
|
||||
"deleteAccount": "Usuń konto",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5 GB za darmo na zdjęcia <bold-green>ente</bold-green>",
|
||||
"loginWithAuthAccount": "Zaloguj się przy użyciu konta Auth",
|
||||
"freeStorageOffer": "10% zniżki na zdjęcia <bold-green>ente</bold-green>",
|
||||
"freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok"
|
||||
"freeStorageOfferDescription": "Użyj kodu „AUTH”, aby uzyskać 10% zniżki na pierwszy rok",
|
||||
"advanced": "Zaawansowane",
|
||||
"algorithm": "Algorytm",
|
||||
"type": "Rodzaj",
|
||||
"period": "Okres",
|
||||
"digits": "Cyfry"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "QR Code inválido",
|
||||
"noRecoveryKeyTitle": "Sem chave de recuperação?",
|
||||
"enterEmailHint": "Insira o endereço de e-mail",
|
||||
"enterNewEmailHint": "Insira seu novo e-mail",
|
||||
"invalidEmailTitle": "Endereço de e-mail inválido",
|
||||
"invalidEmailMessage": "Insira um endereço de e-mail válido.",
|
||||
"deleteAccount": "Excluir conta",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "5GB grátis no <bold-green>ente</bold-green> Photos",
|
||||
"loginWithAuthAccount": "Registrar-se com sua conta Auth",
|
||||
"freeStorageOffer": "10% de desconto no <bold-green>ente</bold-green> photos",
|
||||
"freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano"
|
||||
"freeStorageOfferDescription": "Use o cupom \"AUTH\" para obter 10% de desconto no primeiro ano",
|
||||
"advanced": "Avançado",
|
||||
"algorithm": "Algoritmo",
|
||||
"type": "Tipo",
|
||||
"period": "Período",
|
||||
"digits": "Dígitos"
|
||||
}
|
||||
@@ -6,10 +6,10 @@
|
||||
"@counterAppBarTitle": {
|
||||
"description": "Text shown in the AppBar of the Counter Page"
|
||||
},
|
||||
"onBoardingBody": "2 Faktörlü Kimlik Doğrulama kodlarınızı koruyun",
|
||||
"onBoardingBody": "2FA kodlarınızı güvenli bir şekilde yedekleyin",
|
||||
"onBoardingGetStarted": "Başlayın",
|
||||
"setupFirstAccount": "İlk hesabınızı ekleyin",
|
||||
"importScanQrCode": "Karekod tara",
|
||||
"importScanQrCode": "QR kod tara",
|
||||
"qrCode": "QR Kodu",
|
||||
"importEnterSetupKey": "Kurulum anahtarını giriniz",
|
||||
"importAccountPageTitle": "Hesap bilgilerinizi girin",
|
||||
@@ -51,11 +51,11 @@
|
||||
"trashCode": "Kod çöpe atılsın mı?",
|
||||
"trashCodeMessage": "{account} için kodu çöpe atmak istediğinize emin misiniz?",
|
||||
"trash": "Çöp",
|
||||
"viewLogsAction": "Günlüğü görüntüle",
|
||||
"sendLogsDescription": "Günlüğünüz hatanızı çözmemize yardımcı olacaktır. Hassas bilginin kaydedilmediğine dikkat etsek de bu günlükleri paylaşmadan önce kontrol etmenizi isteriz.",
|
||||
"preparingLogsTitle": "Günlük hazırlanıyor...",
|
||||
"emailLogsTitle": "Günlüğü e-posta olarak gönder",
|
||||
"emailLogsMessage": "Lütfen günlüğünüzü {email} adresine gönderin",
|
||||
"viewLogsAction": "Kayıtları görüntüle",
|
||||
"sendLogsDescription": "Kayıtlarınız hatanızı çözmemize yardımcı olacaktır. Hassas bilginin kaydedilmediğine dikkat etsek de bu günlükleri paylaşmadan önce kontrol etmenizi isteriz.",
|
||||
"preparingLogsTitle": "Kayıtlar hazırlanıyor...",
|
||||
"emailLogsTitle": "Kayıtları e-posta olarak gönder",
|
||||
"emailLogsMessage": "Lütfen kayıtlarınızı {email} adresine gönderin",
|
||||
"@emailLogsMessage": {
|
||||
"placeholders": {
|
||||
"email": {
|
||||
@@ -64,7 +64,7 @@
|
||||
}
|
||||
},
|
||||
"copyEmailAction": "E-postayı Kopyala",
|
||||
"exportLogsAction": "Günlüğü dışa aktar",
|
||||
"exportLogsAction": "Kayıtları dışa aktar",
|
||||
"reportABug": "Hata bildirin",
|
||||
"crashAndErrorReporting": "Çökme ve hata bildirimi",
|
||||
"reportBug": "Hata bildir",
|
||||
@@ -90,7 +90,7 @@
|
||||
"welcomeBack": "Tekrar hoş geldiniz!",
|
||||
"emailAlreadyRegistered": "E-posta zaten kayıtlı.",
|
||||
"emailNotRegistered": "E-posta kayıtlı değil.",
|
||||
"madeWithLoveAtPrefix": "❤️ ile şurada yapılmıştır ",
|
||||
"madeWithLoveAtPrefix": "❤️ ile yapılmıştır ",
|
||||
"supportDevs": "Bu projeyi desteklemek için <bold-green>ente</bold-green> kanalına abone olun",
|
||||
"supportDiscount": "İlk yılda %10 indirim için \"AUTH\" kupon kodunu kullanın",
|
||||
"changeEmail": "E-posta adresini değiştir",
|
||||
@@ -176,7 +176,7 @@
|
||||
"invalidEmailTitle": "Geçersiz e-posta adresi",
|
||||
"invalidEmailMessage": "Lütfen geçerli bir e-posta adresi girin.",
|
||||
"deleteAccount": "Hesabı sil",
|
||||
"deleteAccountQuery": "Sizin gitmenizi görmekten üzüleceğiz. Bazı problemlerle mi karşılaşıyorsunuz?",
|
||||
"deleteAccountQuery": "Sizin gittiğinizi görmekten üzüleceğiz. Bazı problemlerle mi karşılaşıyorsunuz?",
|
||||
"yesSendFeedbackAction": "Evet, geri bildirimi gönder",
|
||||
"noDeleteAccountAction": "Hayır, hesabı sil",
|
||||
"initiateAccountDeleteTitle": "Hesap silme işlemini yapabilmek için lütfen kimliğinizi doğrulayın",
|
||||
@@ -253,8 +253,8 @@
|
||||
"insecureDevice": "Güvenli olmayan cihaz",
|
||||
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Üzgünüz, bu cihazda güvenli anahtarlar oluşturamadık.\n\nlütfen farklı bir cihazdan kaydolun.",
|
||||
"howItWorks": "Nasıl çalışır",
|
||||
"ackPasswordLostWarning": "Eğer şifremi kaybedersem, verilerim <underline>uçtan uca şifrelendiğinden</underline> verilerimi kaybedebileceğimi anladım.",
|
||||
"loginTerms": "Giriş yaparak, <u-terms>kullanım şartları</u-terms>nı ve <u-policy>gizlilik politikası</u-policy>nı onaylıyorum",
|
||||
"ackPasswordLostWarning": "Eğer şifremi kaybedersem, verilerim <underline> uçtan uca şifrelendiğinden </underline> verilerimi kaybedebileceğimi anladım.",
|
||||
"loginTerms": "Giriş yaparak, <u-terms> kullanım şartları </u-terms>nı ve <u-policy> gizlilik politikası </u-policy>nı onaylıyorum",
|
||||
"logInLabel": "Giriş yapın",
|
||||
"logout": "Çıkış yap",
|
||||
"areYouSureYouWantToLogout": "Çıkış yapmak istediğinize emin misiniz?",
|
||||
@@ -277,10 +277,10 @@
|
||||
"recoveryKeyVerifyReason": "Kurtarma anahtarınız, şifrenizi unutmanız durumunda fotoğraflarınızı kurtarmanın tek yoludur. Kurtarma anahtarınızı Ayarlar > Hesap bölümünde bulabilirsiniz.\n\nDoğru kaydettiğinizi doğrulamak için lütfen kurtarma anahtarınızı buraya girin.",
|
||||
"confirmYourRecoveryKey": "Kurtarma anahtarınızı doğrulayın",
|
||||
"confirm": "Doğrula",
|
||||
"emailYourLogs": "Günlüklerinizi e-postayla gönderin",
|
||||
"pleaseSendTheLogsTo": "Lütfen günlükleri şu adrese gönderin\n{toEmail}",
|
||||
"emailYourLogs": "Kayıtlarınızı e-postayla gönderin",
|
||||
"pleaseSendTheLogsTo": "Lütfen kayıtları şu adrese gönderin\n{toEmail}",
|
||||
"copyEmailAddress": "E-posta adresini kopyala",
|
||||
"exportLogs": "Günlüğü dışa aktar",
|
||||
"exportLogs": "Kayıtları dışa aktar",
|
||||
"enterYourRecoveryKey": "Kurtarma anahtarınızı girin",
|
||||
"tempErrorContactSupportIfPersists": "Bir şeyler ters gitmiş gibi görünüyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse, lütfen destek ekibimizle iletişime geçin.",
|
||||
"networkHostLookUpErr": "Ente'ye bağlanılamıyor, lütfen ağ ayarlarınızı kontrol edin ve hata devam ederse desteğe başvurun.",
|
||||
@@ -499,10 +499,24 @@
|
||||
"appLockOfflineModeWarning": "Yedekleme olmadan devam etmeyi seçtiniz. Eğer uygulama parolanızı unutursanız, verilerinize erişiminiz engellenir.",
|
||||
"duplicateCodes": "Yinelenen kodlar",
|
||||
"noDuplicates": "✨ Yinelenen yok",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Temizlenebilecek herhangi bir yinelenen kodunuz yok",
|
||||
"deduplicateCodes": "Kodları tekilleştir",
|
||||
"deselectAll": "Tümünün seçimini kaldır",
|
||||
"selectAll": "Tümünü seç",
|
||||
"deleteDuplicates": "Yinelenenleri sil",
|
||||
"plainHTML": "Sade HTML",
|
||||
"supportEnte": "<bold-Green>Ente</bold-Green>'yi destekle"
|
||||
"tellUsWhatYouThink": "Bize ne düşündüğünü söyle",
|
||||
"dropReviewiOS": "App Store'da bir inceleme bırakın",
|
||||
"dropReviewAndroid": "Play Store'da bir inceleme bırakın",
|
||||
"supportEnte": "<bold-Green>Ente</bold-Green>'yi destekle",
|
||||
"giveUsAStarOnGithub": "Github'da bize bir yıldız verin",
|
||||
"free5GB": "<bold-green>ente</bold-green> Fotoğraflarında 5GB ücretsiz",
|
||||
"loginWithAuthAccount": "Kimlik Doğrulama hesabınızla giriş yapın",
|
||||
"freeStorageOffer": "<bold-green>ente</bold-green> fotoğraflarında %10 indirim",
|
||||
"freeStorageOfferDescription": "İlk yılda %10 indirim almak için \"AUTH\" kodunu kullanın",
|
||||
"advanced": "Gelişmiş",
|
||||
"algorithm": "Algoritma",
|
||||
"type": "Tür",
|
||||
"period": "Zaman Aralığı",
|
||||
"digits": "Uzunluk"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "二维码无效",
|
||||
"noRecoveryKeyTitle": "没有恢复密钥吗?",
|
||||
"enterEmailHint": "请输入您的电子邮件地址",
|
||||
"enterNewEmailHint": "请输入您的新电子邮件地址",
|
||||
"invalidEmailTitle": "无效的电子邮件地址",
|
||||
"invalidEmailMessage": "请输入一个有效的电子邮件地址。",
|
||||
"deleteAccount": "删除账户",
|
||||
@@ -513,5 +514,10 @@
|
||||
"free5GB": "<bold-green>ente</bold-green> Photos 上 5GB 可用空间",
|
||||
"loginWithAuthAccount": "使用您的认证账户登录",
|
||||
"freeStorageOffer": "购买 <bold-green>ente</bold-green> Photos 可享受 10% 优惠",
|
||||
"freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣"
|
||||
"freeStorageOfferDescription": "使用优惠码“AUTH”可享受首年 10% 折扣",
|
||||
"advanced": "高级",
|
||||
"algorithm": "算法",
|
||||
"type": "类型",
|
||||
"period": "周期",
|
||||
"digits": "数字"
|
||||
}
|
||||
@@ -513,5 +513,10 @@
|
||||
"free5GB": "<bold-green>ente</bold-green> Photos 上 5GB 可用空間",
|
||||
"loginWithAuthAccount": "使用您的認證帳戶登錄",
|
||||
"freeStorageOffer": "購買 <bold-green>ente</bold-green> Photos 可享受 10% 優惠",
|
||||
"freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣"
|
||||
"freeStorageOfferDescription": "使用優惠碼“AUTH”可享受首年 10% 折扣",
|
||||
"advanced": "進階",
|
||||
"algorithm": "演算法",
|
||||
"type": "類型",
|
||||
"period": "期間",
|
||||
"digits": "數位"
|
||||
}
|
||||
@@ -38,28 +38,28 @@ const List<Locale> appSupportedLocales = <Locale>[
|
||||
];
|
||||
|
||||
Locale? autoDetectedLocale;
|
||||
Locale localResolutionCallBack(locales, supportedLocales) {
|
||||
Locale? languageCodeMatch;
|
||||
final Map<String, Locale> languageCodeToLocale = {
|
||||
for (Locale supportedLocale in appSupportedLocales)
|
||||
supportedLocale.languageCode: supportedLocale,
|
||||
};
|
||||
|
||||
for (Locale locale in locales) {
|
||||
// This function takes device locales and supported locales as input
|
||||
// and returns the best matching locale.
|
||||
// The device locales are sorted by priority, so the first one is the most preferred.
|
||||
Locale localResolutionCallBack(onDeviceLocales, supportedLocales) {
|
||||
final Set<String> languageSupport = {};
|
||||
for (Locale supportedLocale in appSupportedLocales) {
|
||||
languageSupport.add(supportedLocale.languageCode);
|
||||
}
|
||||
for (Locale locale in onDeviceLocales) {
|
||||
// check if exact local is supported, if yes, return it
|
||||
if (appSupportedLocales.contains(locale)) {
|
||||
autoDetectedLocale = locale;
|
||||
return locale;
|
||||
}
|
||||
|
||||
if (languageCodeMatch == null &&
|
||||
languageCodeToLocale.containsKey(locale.languageCode)) {
|
||||
languageCodeMatch = languageCodeToLocale[locale.languageCode];
|
||||
autoDetectedLocale = languageCodeMatch;
|
||||
// check if language code is supported, if yes, return it
|
||||
if (languageSupport.contains(locale.languageCode)) {
|
||||
autoDetectedLocale = locale;
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the first language code match or default to 'en'
|
||||
return languageCodeMatch ?? const Locale('en');
|
||||
return autoDetectedLocale ?? const Locale('en');
|
||||
}
|
||||
|
||||
Future<Locale?> getLocale({
|
||||
|
||||
@@ -293,10 +293,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
widget.code == null
|
||||
? advanceOptionWidget()
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
alignment: WrapAlignment.start,
|
||||
@@ -343,6 +339,10 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
widget.code == null
|
||||
? advanceOptionWidget()
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: 400,
|
||||
@@ -525,6 +525,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
}
|
||||
|
||||
Widget advanceOptionWidget() {
|
||||
final l10n = context.l10n;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Column(
|
||||
@@ -537,9 +538,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Advanced',
|
||||
),
|
||||
Text(l10n.advanced),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: showAdvancedOptions,
|
||||
builder: (context, isExpanded, child) {
|
||||
@@ -583,7 +582,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const FieldLabel("Algorithm", width: 60),
|
||||
FieldLabel(l10n.algorithm, width: 60),
|
||||
AlgorithmSelectorWidget(
|
||||
currentAlgorithm: _algorithm,
|
||||
onSelected: (newAlgorithm) async {
|
||||
@@ -597,7 +596,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const FieldLabel("Type", width: 60),
|
||||
FieldLabel(l10n.type, width: 60),
|
||||
ToptSelectorWidget(
|
||||
currentTopt: _type,
|
||||
onSelected: (newTopt) async {
|
||||
@@ -610,7 +609,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const FieldLabel("Period", width: 60),
|
||||
FieldLabel(l10n.period, width: 60),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
@@ -639,7 +638,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const FieldLabel("Digits", width: 60),
|
||||
FieldLabel(l10n.digits, width: 60),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
|
||||
@@ -18,7 +18,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return AlertDialog(
|
||||
title: Text(l10n.enterEmailHint),
|
||||
title: Text(l10n.enterNewEmailHint),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="4.3.8" date="2025-05-20" />
|
||||
<release version="4.2.4" date="2025-01-11" />
|
||||
<release version="4.0.3" date="2024-10-08" />
|
||||
</releases>
|
||||
@@ -33,4 +34,4 @@
|
||||
<color type="primary" scheme_preference="light">#ffffff</color>
|
||||
<color type="primary" scheme_preference="dark">#000000</color>
|
||||
</branding>
|
||||
</component>
|
||||
</component>
|
||||
|
||||
@@ -5,15 +5,15 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
version: "72.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "0.3.2"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -26,10 +26,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "6.7.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -90,10 +90,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.12.0"
|
||||
version: "2.11.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -130,10 +130,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.1"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -202,10 +202,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.3.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -234,10 +234,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.1"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -250,10 +250,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
version: "1.18.0"
|
||||
confetti:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -435,10 +435,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -944,18 +944,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.8"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.9"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1024,18 +1024,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
version: "0.1.2-main.4"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1056,10 +1056,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
version: "1.15.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1168,10 +1168,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.9.0"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1472,7 +1472,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
version: "0.0.99"
|
||||
sodium:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1509,10 +1509,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1566,10 +1566,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.1"
|
||||
version: "1.11.1"
|
||||
steam_totp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1590,10 +1590,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.2"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1606,10 +1606,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
version: "1.2.0"
|
||||
styled_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1630,18 +1630,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
version: "1.2.1"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
version: "0.7.2"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1806,10 +1806,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.1"
|
||||
version: "14.2.5"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1899,5 +1899,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 4.3.6+437
|
||||
version: 4.3.8+438
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
70
auth/scripts/release_tag.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 tag"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure a tag was provided
|
||||
[[ $# -eq 0 ]] && usage
|
||||
|
||||
# Exit immediately if a command exits with a non-zero status
|
||||
set -e
|
||||
|
||||
# Go to the project root directory
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Get the tag from the command line argument
|
||||
TAG=$1
|
||||
|
||||
# Define the appdata file path - use absolute path to avoid directory navigation issues
|
||||
PROJECT_ROOT=$(pwd)
|
||||
APPDATA_FILE="${PROJECT_ROOT}/linux/packaging/enteauth.appdata.xml"
|
||||
|
||||
# Get the version from the pubspec.yaml file and cut everything after the +
|
||||
VERSION=$(grep "^version:" pubspec.yaml | awk '{ print $2 }' | cut -d '+' -f 1)
|
||||
|
||||
PREFIX="auth-v"
|
||||
|
||||
# Ensure the tag has the correct prefix
|
||||
if [[ $TAG != $PREFIX* ]]; then
|
||||
echo "Invalid tag. tags must start with '$PREFIX'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure the tag version is in the pubspec.yaml file
|
||||
if [[ $TAG != *$VERSION ]]; then
|
||||
echo "Invalid tag."
|
||||
echo "The version $VERSION in pubspec doesn't match the version in tag $TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version number from the tag (remove prefix)
|
||||
TAG_VERSION=${TAG#$PREFIX}
|
||||
|
||||
# Check if this version is already in the releases section of the appdata.xml file
|
||||
if ! grep -q "<release version=\"$TAG_VERSION\"" "$APPDATA_FILE"; then
|
||||
echo "Adding release entry for version $TAG_VERSION to appdata.xml"
|
||||
|
||||
# Get today's date in YYYY-MM-DD format
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
|
||||
# Use a more reliable approach with awk instead of sed for cross-platform compatibility
|
||||
echo "Creating temporary file with updated content..."
|
||||
awk '/<releases>/{print $0; print " <release version=\"'"$TAG_VERSION"'\" date=\"'"$TODAY"'\" />"; next}1' "$APPDATA_FILE" > "${APPDATA_FILE}.tmp"
|
||||
mv "${APPDATA_FILE}.tmp" "$APPDATA_FILE"
|
||||
|
||||
echo "Added release entry for version $TAG_VERSION with date $TODAY"
|
||||
|
||||
# Stage and commit the updated appdata.xml file
|
||||
git add "$APPDATA_FILE"
|
||||
git commit -m "Add release $TAG_VERSION to appdata.xml"
|
||||
echo "Committed appdata.xml changes for version $TAG_VERSION"
|
||||
fi
|
||||
|
||||
# If all checks pass, create the tag
|
||||
git tag $TAG
|
||||
echo "Tag $TAG created."
|
||||
|
||||
exit 0
|
||||
@@ -83,13 +83,14 @@ func (c *Client) VerifySRPSession(
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (c *Client) SendEmailOTP(
|
||||
func (c *Client) SendLoginOTP(
|
||||
ctx context.Context,
|
||||
email string,
|
||||
) error {
|
||||
var res AuthorizationResponse
|
||||
payload := map[string]interface{}{
|
||||
"email": email,
|
||||
"email": email,
|
||||
"purpose": "login",
|
||||
}
|
||||
r, err := c.restClient.R().
|
||||
SetContext(ctx).
|
||||
|
||||
@@ -167,7 +167,7 @@ func (c *ClICtrl) verifyPassKey(ctx context.Context, authResp *api.Authorization
|
||||
}
|
||||
|
||||
func (c *ClICtrl) validateEmail(ctx context.Context, email string) (*api.AuthorizationResponse, error) {
|
||||
err := c.Client.SendEmailOTP(ctx, email)
|
||||
err := c.Client.SendLoginOTP(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## v1.7.13 (Unreleased)
|
||||
|
||||
- Generate streams for videos (beta)
|
||||
|
||||
> Streamable videos can be enabled in Preferences. For more details, see the
|
||||
> [video streaming FAQ](https://help.ente.io/photos/faq/video-streaming).
|
||||
|
||||
- Support Turkish translations.
|
||||
- .
|
||||
|
||||
## v1.7.12
|
||||
|
||||
@@ -38,25 +38,26 @@
|
||||
"lru-cache": "^11.1.0",
|
||||
"next-electron-server": "^1.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"onnxruntime-node": "^1.20.1"
|
||||
"onnxruntime-node": "^1.20.1",
|
||||
"zod": "^3.25.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/auto-launch": "^5.0.5",
|
||||
"@types/ffmpeg-static": "^3.0.3",
|
||||
"ajv": "^8.17.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^36.1.0",
|
||||
"electron": "^36.3.1",
|
||||
"electron-builder": "^26.0.14",
|
||||
"eslint": "^9",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"shx": "^0.3.4",
|
||||
"prettier-plugin-packagejson": "^2.5.14",
|
||||
"shx": "^0.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.1"
|
||||
"typescript-eslint": "^8.32.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"productName": "ente"
|
||||
|
||||
@@ -78,6 +78,14 @@ export const allowWindowClose = (): void => {
|
||||
* We call this at the end of this file.
|
||||
*/
|
||||
const main = () => {
|
||||
// Workaround for Electron 36 not launching on some Linux distros. Remove
|
||||
// once fixed or otherwise mitigated upstream.
|
||||
//
|
||||
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
|
||||
if (process.platform == "linux") {
|
||||
app.commandLine.appendSwitch("gtk-version", "3");
|
||||
}
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
FFmpegCommand,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
UtilityProcessType,
|
||||
ZipItem,
|
||||
} from "../types/ipc";
|
||||
import { logToDisk } from "./log";
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
} from "./services/dir";
|
||||
import { ffmpegExec } from "./services/ffmpeg";
|
||||
import { ffmpegDetermineVideoDuration, ffmpegExec } from "./services/ffmpeg";
|
||||
import {
|
||||
fsExists,
|
||||
fsFindFiles,
|
||||
@@ -47,7 +48,6 @@ import {
|
||||
} from "./services/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./services/image";
|
||||
import { logout } from "./services/logout";
|
||||
import { createMLWorker } from "./services/ml";
|
||||
import {
|
||||
lastShownChangelogVersion,
|
||||
masterKeyB64,
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
watchUpdateIgnoredFiles,
|
||||
watchUpdateSyncedFiles,
|
||||
} from "./services/watch";
|
||||
import { triggerCreateUtilityProcess } from "./services/workers";
|
||||
|
||||
/**
|
||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||
@@ -181,10 +182,10 @@ export const attachIPCHandlers = () => {
|
||||
"generateImageThumbnail",
|
||||
(
|
||||
_,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) => generateImageThumbnail(dataOrPathOrZipItem, maxDimension, maxSize),
|
||||
) => generateImageThumbnail(pathOrZipItem, maxDimension, maxSize),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
@@ -192,9 +193,15 @@ export const attachIPCHandlers = () => {
|
||||
(
|
||||
_,
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
) => ffmpegExec(command, dataOrPathOrZipItem, outputFileExtension),
|
||||
) => ffmpegExec(command, pathOrZipItem, outputFileExtension),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"ffmpegDetermineVideoDuration",
|
||||
(_, pathOrZipItem: string | ZipItem) =>
|
||||
ffmpegDetermineVideoDuration(pathOrZipItem),
|
||||
);
|
||||
|
||||
// - Upload
|
||||
@@ -233,9 +240,11 @@ export const attachIPCHandlers = () => {
|
||||
* the main window to do their thing.
|
||||
*/
|
||||
export const attachMainWindowIPCHandlers = (mainWindow: BrowserWindow) => {
|
||||
// - ML
|
||||
// - Utility processes
|
||||
|
||||
ipcMain.on("createMLWorker", () => createMLWorker(mainWindow));
|
||||
ipcMain.on("triggerCreateUtilityProcess", (_, type: UtilityProcessType) =>
|
||||
triggerCreateUtilityProcess(type, mainWindow),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
59
desktop/src/main/log-worker.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* A object that behaves similar to the default export of "./log", except this
|
||||
* can be used from within a utility process.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* We cannot directly do
|
||||
*
|
||||
* import log from "../log";
|
||||
*
|
||||
* because that requires the Electron APIs that are not available to a utility
|
||||
* process (See: [Note: Using Electron APIs in UtilityProcess]).
|
||||
*
|
||||
* But even if that were to work, logging will still be problematic since we'd
|
||||
* try opening the log file from two different Node.js processes (this one, and
|
||||
* the main one), and I didn't find any indication in the electron-log
|
||||
* repository that the log file's integrity would be maintained in such cases.
|
||||
*
|
||||
* So instead we provide this proxy log object that uses the
|
||||
* `process.parentPort` to transport the logs over to the main process, where
|
||||
* the {@link processUtilityProcessLogMessage} function in the main process is
|
||||
* expected to handle these (sending them to the actual log).
|
||||
*/
|
||||
export default {
|
||||
error: (s: string, e?: unknown) =>
|
||||
mainProcess("log.errorString", messageWithError(s, e)),
|
||||
warn: (s: string, e?: unknown) =>
|
||||
mainProcess("log.warnString", messageWithError(s, e)),
|
||||
info: (...ms: unknown[]) => mainProcess("log.info", ms),
|
||||
/**
|
||||
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
|
||||
* accepts only strings.
|
||||
*/
|
||||
debugString: (s: string) => mainProcess("log.debugString", s),
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a message to the main process using a barebones RPC protocol.
|
||||
*/
|
||||
const mainProcess = (method: string, param: unknown) =>
|
||||
process.parentPort.postMessage({ method, p: param });
|
||||
|
||||
// Duplicated verbatim from ./log.ts
|
||||
const messageWithError = (message: string, e?: unknown) => {
|
||||
if (!e) return message;
|
||||
|
||||
let es: string;
|
||||
if (e instanceof Error) {
|
||||
// In practice, we expect ourselves to be called with Error objects, so
|
||||
// this is the happy path so to say.
|
||||
es = [`${e.name}: ${e.message}`, e.stack].filter((x) => x).join("\n");
|
||||
} else {
|
||||
// For the rest rare cases, use the default string serialization of e.
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
es = String(e);
|
||||
}
|
||||
|
||||
return `${message}: ${es}`;
|
||||
};
|
||||
@@ -83,6 +83,56 @@ const logDebug = (param: () => unknown) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle log messages posted from the utility process in the main process.
|
||||
*
|
||||
* See: [Note: Using Electron APIs in UtilityProcess]
|
||||
*
|
||||
* @param message The arbitrary message that was received as an argument to the
|
||||
* "message" event invoked on a {@link UtilityProcess}.
|
||||
*
|
||||
* @returns true if the message was recognized and handled, and false otherwise.
|
||||
*/
|
||||
export const processUtilityProcessLogMessage = (
|
||||
logTag: string,
|
||||
message: unknown,
|
||||
) => {
|
||||
const m = message; /* shorter alias */
|
||||
if (m && typeof m == "object" && "method" in m && "p" in m) {
|
||||
const p = m.p;
|
||||
switch (m.method) {
|
||||
case "log.errorString":
|
||||
if (typeof p == "string") {
|
||||
logError(`${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.warnString":
|
||||
if (typeof p == "string") {
|
||||
logWarn(`${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.info":
|
||||
if (Array.isArray(p)) {
|
||||
// Need to cast from any[] to unknown[]
|
||||
logInfo(logTag, ...(p as unknown[]));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.debugString":
|
||||
if (typeof p == "string") {
|
||||
logDebug(() => `${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ente's logger.
|
||||
*
|
||||
|
||||
1132
desktop/src/main/services/ffmpeg-worker.ts
Normal file
@@ -1,678 +1,77 @@
|
||||
import pathToFfmpeg from "ffmpeg-static";
|
||||
import { randomBytes } from "node:crypto";
|
||||
/**
|
||||
* @file A bridge to the ffmpeg utility process. This code runs in the main
|
||||
* process.
|
||||
*/
|
||||
|
||||
import { wrap } from "comlink";
|
||||
import fs from "node:fs/promises";
|
||||
import path, { basename } from "node:path";
|
||||
import type { FFmpegCommand, ZipItem } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { execAsync } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeFileForStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
/* Ditto in the web app's code (used by the Wasm FFmpeg invocation). */
|
||||
const ffmpegPathPlaceholder = "FFMPEG";
|
||||
const inputPathPlaceholder = "INPUT";
|
||||
const outputPathPlaceholder = "OUTPUT";
|
||||
import type { FFmpegUtilityProcess } from "./ffmpeg-worker";
|
||||
import { ffmpegUtilityProcessEndpoint } from "./workers";
|
||||
|
||||
/**
|
||||
* Run a FFmpeg command
|
||||
*
|
||||
* [Note: FFmpeg in Electron]
|
||||
*
|
||||
* There is a Wasm build of FFmpeg, but that is currently 10-20 times slower
|
||||
* that the native build. That is slow enough to be unusable for our purposes.
|
||||
* https://ffmpegwasm.netlify.app/docs/performance
|
||||
*
|
||||
* So the alternative is to bundle a FFmpeg executable binary with our app. e.g.
|
||||
*
|
||||
* yarn add fluent-ffmpeg ffmpeg-static ffprobe-static
|
||||
*
|
||||
* (we only use ffmpeg-static, the rest are mentioned for completeness' sake).
|
||||
*
|
||||
* Interestingly, Electron already bundles an binary FFmpeg library (it comes
|
||||
* from the ffmpeg fork maintained by Chromium).
|
||||
* https://chromium.googlesource.com/chromium/third_party/ffmpeg
|
||||
* https://stackoverflow.com/questions/53963672/what-version-of-ffmpeg-is-bundled-inside-electron
|
||||
*
|
||||
* This can be found in (e.g. on macOS) at
|
||||
*
|
||||
* $ file ente.app/Contents/Frameworks/Electron\ Framework.framework/Versions/Current/Libraries/libffmpeg.dylib
|
||||
* .../libffmpeg.dylib: Mach-O 64-bit dynamically linked shared library arm64
|
||||
*
|
||||
* But I'm not sure if our code is supposed to be able to use it, and how.
|
||||
* Return a handle to the ffmpeg utility process, starting it if needed.
|
||||
*/
|
||||
export const ffmpegUtilityProcess = () =>
|
||||
ffmpegUtilityProcessEndpoint().then((port) =>
|
||||
wrap<FFmpegUtilityProcess>(port),
|
||||
);
|
||||
|
||||
/**
|
||||
* Implement the IPC "ffmpegExec" contract, writing the input and output to
|
||||
* temporary files as needed, and then forward to the {@link ffmpegExec} running
|
||||
* in the utility process.
|
||||
*/
|
||||
export const ffmpegExec = async (
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
): Promise<Uint8Array> => {
|
||||
): Promise<Uint8Array> =>
|
||||
withInputFile(pathOrZipItem, async (worker, inputFilePath) => {
|
||||
const outputFilePath = await makeTempFilePath(outputFileExtension);
|
||||
try {
|
||||
await worker.ffmpegExec(command, inputFilePath, outputFilePath);
|
||||
return await fs.readFile(outputFilePath);
|
||||
} finally {
|
||||
await deleteTempFileIgnoringErrors(outputFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
export const withInputFile = async <T>(
|
||||
pathOrZipItem: string | ZipItem,
|
||||
f: (worker: FFmpegUtilityProcess, inputFilePath: string) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const worker = await ffmpegUtilityProcess();
|
||||
|
||||
const {
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
} = await makeFileForStreamOrPathOrZipItem(pathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath(outputFileExtension);
|
||||
try {
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
let resolvedCommand: string[];
|
||||
if (Array.isArray(command)) {
|
||||
resolvedCommand = command;
|
||||
} else {
|
||||
const isHDR = await isHDRVideo(inputFilePath);
|
||||
log.debug(() => [basename(inputFilePath), { isHDR }]);
|
||||
resolvedCommand = isHDR ? command.hdr : command.default;
|
||||
}
|
||||
|
||||
const cmd = substitutePlaceholders(
|
||||
resolvedCommand,
|
||||
inputFilePath,
|
||||
outputFilePath,
|
||||
);
|
||||
|
||||
await execAsync(cmd);
|
||||
|
||||
return await fs.readFile(outputFilePath);
|
||||
return await f(worker, inputFilePath);
|
||||
} finally {
|
||||
if (isInputFileTemporary)
|
||||
await deleteTempFileIgnoringErrors(inputFilePath);
|
||||
await deleteTempFileIgnoringErrors(outputFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
const substitutePlaceholders = (
|
||||
command: string[],
|
||||
inputFilePath: string,
|
||||
outputFilePath: string,
|
||||
) =>
|
||||
command.map((segment) => {
|
||||
if (segment == ffmpegPathPlaceholder) {
|
||||
return ffmpegBinaryPath();
|
||||
} else if (segment == inputPathPlaceholder) {
|
||||
return inputFilePath;
|
||||
} else if (segment == outputPathPlaceholder) {
|
||||
return outputFilePath;
|
||||
} else {
|
||||
return segment;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the path to the `ffmpeg` binary.
|
||||
*
|
||||
* At runtime, the FFmpeg binary is present in a path like (macOS example):
|
||||
* `ente.app/Contents/Resources/app.asar.unpacked/node_modules/ffmpeg-static/ffmpeg`
|
||||
* Implement the IPC "ffmpegDetermineVideoDuration" contract, writing the input
|
||||
* to temporary files as needed, and then forward to the
|
||||
* {@link ffmpegDetermineVideoDuration} running in the utility process.
|
||||
*/
|
||||
const ffmpegBinaryPath = () => {
|
||||
// This substitution of app.asar by app.asar.unpacked is suggested by the
|
||||
// ffmpeg-static library author themselves:
|
||||
// https://github.com/eugeneware/ffmpeg-static/issues/16
|
||||
return pathToFfmpeg!.replace("app.asar", "app.asar.unpacked");
|
||||
};
|
||||
|
||||
/**
|
||||
* A variant of {@link ffmpegExec} adapted to work with streams so that it can
|
||||
* handle the MP4 conversion of large video files.
|
||||
*
|
||||
* @param inputFilePath The path to a file on the user's local file system. This
|
||||
* is the video we want to convert.
|
||||
*
|
||||
* @param outputFilePath The path to a file on the user's local file system where
|
||||
* we should write the converted MP4 video.
|
||||
*/
|
||||
export const ffmpegConvertToMP4 = async (
|
||||
inputFilePath: string,
|
||||
outputFilePath: string,
|
||||
): Promise<void> => {
|
||||
const command = [
|
||||
ffmpegPathPlaceholder,
|
||||
"-i",
|
||||
inputPathPlaceholder,
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
outputPathPlaceholder,
|
||||
];
|
||||
|
||||
const cmd = substitutePlaceholders(command, inputFilePath, outputFilePath);
|
||||
|
||||
await execAsync(cmd);
|
||||
};
|
||||
|
||||
export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
playlistPath: string;
|
||||
videoPath: string;
|
||||
dimensions: { width: number; height: number };
|
||||
videoSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for
|
||||
* videos.
|
||||
*
|
||||
* Overview of the cases:
|
||||
*
|
||||
* H.264, <= 10 MB - Skip
|
||||
* H.264, <= 4000 kb/s bitrate - Don't re-encode video stream
|
||||
* BT.709, <= 2000 kb/s bitrate - Don't apply the scale+fps filter
|
||||
* !BT.709 - Apply tonemap (zscale+tonemap+zscale)
|
||||
*
|
||||
* Example invocation:
|
||||
*
|
||||
* ffmpeg -i in.mov -vf 'scale=-2:720,fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p' -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8
|
||||
*
|
||||
* See: [Note: Preview variant of videos]
|
||||
*
|
||||
* @param inputFilePath The path to a file on the user's local file system. This
|
||||
* is the video we want to generate an streamable HLS playlist for.
|
||||
*
|
||||
* @param outputPathPrefix The path to unique, unused and temporary prefix on
|
||||
* the user's local file system. This function will write the generated HLS
|
||||
* playlist and video segments under this prefix.
|
||||
*
|
||||
* @returns The paths to two files on the user's local file system - one
|
||||
* containing the generated HLS playlist, and the other containing the
|
||||
* transcoded and encrypted video segments that the HLS playlist refers to.
|
||||
*
|
||||
* If the video is such that it doesn't require stream generation, then this
|
||||
* function returns `undefined`.
|
||||
*/
|
||||
export const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
inputFilePath: string,
|
||||
outputPathPrefix: string,
|
||||
): Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined> => {
|
||||
const { isH264, isBT709, bitrate } =
|
||||
await detectVideoCharacteristics(inputFilePath);
|
||||
|
||||
log.debug(() => [basename(inputFilePath), { isH264, isBT709, bitrate }]);
|
||||
|
||||
// If the video is smaller than 10 MB, and already H.264 (the codec we are
|
||||
// going to use for the conversion), then a streaming variant is not much
|
||||
// use. Skip such cases.
|
||||
//
|
||||
// ---
|
||||
//
|
||||
// [Note: HEVC/H.265 issues]
|
||||
//
|
||||
// We've observed two issues out in the wild with HEVC videos:
|
||||
//
|
||||
// 1. On Linux, HEVC video streams don't play. However, since the audio
|
||||
// stream plays, the browser tells us that the "video" itself is
|
||||
// playable, but the user sees a blank screen with only audio.
|
||||
//
|
||||
// 2. HEVC + HDR videos taken on an iPhone have a rotation (`Side data:
|
||||
// displaymatrix` in the ffmpeg output) that Chrome (and thus Electron)
|
||||
// doesn't take into account, so these play upside down.
|
||||
//
|
||||
// Not fully related to this case, but mentioning here as to why both the
|
||||
// size and codec need to be checked before skipping stream generation.
|
||||
if (isH264) {
|
||||
const inputVideoSize = await fs
|
||||
.stat(inputFilePath)
|
||||
.then((st) => st.size);
|
||||
if (inputVideoSize <= 10 * 1024 * 1024 /* 10 MB */) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If the video is already H.264 with a bitrate less than 4000 kbps, then we
|
||||
// do not need to reencode the video stream (by _far_ the costliest part of
|
||||
// the HLS stream generation).
|
||||
const reencodeVideo = !(isH264 && bitrate && bitrate <= 4000 * 1000);
|
||||
|
||||
// If the bitrate is not too high, then we don't need to rescale the video
|
||||
// when generating the video stream. This is not a performance optimization,
|
||||
// but more for avoiding making the video size smaller unnecessarily.
|
||||
const rescaleVideo = !(bitrate && bitrate <= 2000 * 1000);
|
||||
|
||||
// [Note: Tonemapping HDR to HD]
|
||||
//
|
||||
// BT.709 ("HD") is a standard that describes things like how color is
|
||||
// encoded, the range of values, and their "meaning" - i.e. how to map the
|
||||
// values in the video to the pixels on the screen.
|
||||
//
|
||||
// It is not the only such standard, there are three common examples:
|
||||
//
|
||||
// - BT.601 ("Standard-Definition" or SD)
|
||||
// - BT.709 ("High-Definition" or HD)
|
||||
// - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^).
|
||||
//
|
||||
// ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for our
|
||||
// purpose here we can treat it as as alias.
|
||||
//
|
||||
// BT.709 is the most common amongst these for older files out stored on
|
||||
// computers, and they conform mostly to the standard (one notable exception
|
||||
// is that the BT.709 standard also recommends using the yuv422p pixel
|
||||
// format, but de facto yuv420p is used because many video players only
|
||||
// support yuv420p).
|
||||
//
|
||||
// Since BT.709 is the most widely supported standard, we use it when
|
||||
// generating the HLS playlist so to allow playback across the widest
|
||||
// possible hardware/OS/browser combinations.
|
||||
//
|
||||
// If we convert HDR to HD without naively, then the colors look washed out
|
||||
// compared to the original. To resolve this, we use a ffmpeg filterchain
|
||||
// that uses the tonemap filter.
|
||||
//
|
||||
// However applying this tonemap to videos that are already HD leads to a
|
||||
// brightness drop. So we conditionally apply this filter chain only if the
|
||||
// colorspace is not already BT.709.
|
||||
//
|
||||
// See also: [Note: Alternative FFmpeg command for HDR videos], although
|
||||
// that uses a allow-list based check (while here we use deny-list).
|
||||
//
|
||||
// Reference:
|
||||
// - https://trac.ffmpeg.org/wiki/colorspace
|
||||
const tonemap = !isBT709;
|
||||
|
||||
// We want the generated playlist to refer to the chunks as "output.ts".
|
||||
//
|
||||
// So we arrange things accordingly: We use the `outputPathPrefix` as our
|
||||
// working directory, and then ask ffmpeg to generate a playlist with the
|
||||
// name "output.m3u8".
|
||||
//
|
||||
// ffmpeg will automatically place the segments in a file with the same base
|
||||
// name as the playlist, but with a ".ts" extension. And since we use the
|
||||
// "single_file" option, all the segments will be placed in a file named
|
||||
// "output.ts".
|
||||
|
||||
await fs.mkdir(outputPathPrefix);
|
||||
|
||||
const playlistPath = path.join(outputPathPrefix, "output.m3u8");
|
||||
const videoPath = path.join(outputPathPrefix, "output.ts");
|
||||
|
||||
// Generate a cryptographically secure random key (16 bytes).
|
||||
const keyBytes = randomBytes(16);
|
||||
const keyB64 = keyBytes.toString("base64");
|
||||
|
||||
// Convert it to a data: URI that will be added to the playlist.
|
||||
const keyURI = `data:text/plain;base64,${keyB64}`;
|
||||
|
||||
// Determine two paths - one where we will write the key itself, and where
|
||||
// we will write the "key info" that provides ffmpeg the `keyURI` and the
|
||||
// `keyPath;.
|
||||
const keyPath = playlistPath + ".key";
|
||||
const keyInfoPath = playlistPath + ".key-info";
|
||||
|
||||
// Generate a "key info":
|
||||
//
|
||||
// - the first line specifies the key URI that is written into the playlist.
|
||||
// - the second line specifies the path to the local file system file from
|
||||
// where ffmpeg should read the key.
|
||||
const keyInfo = [keyURI, keyPath].join("\n");
|
||||
|
||||
// Overview:
|
||||
//
|
||||
// - Video H.264 HD 720p 30fps.
|
||||
// - Audio AAC 128kbps.
|
||||
// - Encrypted HLS playlist with a single file containing all the chunks.
|
||||
//
|
||||
// Reference:
|
||||
// - `man ffmpeg-all`
|
||||
// - https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
//
|
||||
const command = [
|
||||
ffmpegBinaryPath(),
|
||||
// Reduce the amount of output lines we have to parse.
|
||||
["-hide_banner"],
|
||||
// Input file. We don't need any extra options that apply to the input file.
|
||||
"-i",
|
||||
inputFilePath,
|
||||
// The remaining options apply to the next output file (`playlistPath`).
|
||||
reencodeVideo
|
||||
? [
|
||||
// `-vf` creates a filter graph for the video stream. It is a
|
||||
// comma separated list of filters chained together, e.g.
|
||||
// `filter1=key=value:key=value.filter2=key=value`.
|
||||
"-vf",
|
||||
[
|
||||
// Do the rescaling to even number of pixels always if the
|
||||
// tonemapping is going to be applied subsequently,
|
||||
// otherwise the tonemapping will fail with "image
|
||||
// dimensions must be divisible by subsampling factor".
|
||||
//
|
||||
// While we add the extra condition here for completeness,
|
||||
// it won't usually matter since a non-BT.709 video is
|
||||
// likely using a new codec, and as such would've a high
|
||||
// enough bitrate to require rescaling anyways.
|
||||
rescaleVideo || tonemap
|
||||
? [
|
||||
// Scales the video to maximum 720p height,
|
||||
// keeping aspect ratio and the calculated
|
||||
// dimension divisible by 2 (some of the other
|
||||
// operations require an even pixel count).
|
||||
"scale=-2:720",
|
||||
// Convert the video to a constant 30 fps,
|
||||
// duplicating or dropping frames as necessary.
|
||||
"fps=30",
|
||||
]
|
||||
: [],
|
||||
// Convert the colorspace if the video is not in the HD
|
||||
// color space (bt709). Before conversion, tone map colors
|
||||
// so that they work the same across the change in the
|
||||
// dyamic range.
|
||||
//
|
||||
// 1. The tonemap filter only works linear light, so we
|
||||
// first use zscale with transfer=linear to linearize
|
||||
// the input.
|
||||
//
|
||||
// 2. Then we use the tonemap, with the hable option that
|
||||
// is best for preserving details. desat=0 turns off
|
||||
// the default desaturation.
|
||||
//
|
||||
// 3. Use zscale again to "convert to BT.709" by asking it
|
||||
// to set the all three of color primaries, transfer
|
||||
// characteristics and colorspace matrix to 709 (Note:
|
||||
// the constants specified in the tonemap filter help
|
||||
// do not include the "bt" prefix)
|
||||
//
|
||||
// See: https://ffmpeg.org/ffmpeg-filters.html#tonemap-1
|
||||
//
|
||||
// See: [Note: Tonemapping HDR to HD]
|
||||
tonemap
|
||||
? [
|
||||
"zscale=transfer=linear",
|
||||
"tonemap=tonemap=hable:desat=0",
|
||||
"zscale=primaries=709:transfer=709:matrix=709",
|
||||
]
|
||||
: [],
|
||||
// Output using the well supported pixel format: 8-bit YUV
|
||||
// planar color space with 4:2:0 chroma subsampling.
|
||||
"format=yuv420p",
|
||||
]
|
||||
.flat()
|
||||
.join(","),
|
||||
]
|
||||
: [],
|
||||
reencodeVideo
|
||||
? // Video codec H.264
|
||||
//
|
||||
// - `-c:v libx264` converts the video stream to the H.264 codec.
|
||||
//
|
||||
// - We don't supply a bitrate, instead it uses the default CRF
|
||||
// ("23") as recommended in the ffmpeg trac.
|
||||
//
|
||||
// - We don't supply a preset, it'll use the default ("medium").
|
||||
["-c:v", "libx264"]
|
||||
: // Keep the video stream unchanged
|
||||
["-c:v", "copy"],
|
||||
// Audio codec AAC
|
||||
//
|
||||
// - `-c:a aac` converts the audio stream to use the AAC codec
|
||||
//
|
||||
// - We don't supply a bitrate, it'll use the AAC default 128k bps.
|
||||
["-c:a", "aac"],
|
||||
// Generate a HLS playlist.
|
||||
["-f", "hls"],
|
||||
// Tell ffmpeg where to find the key, and the URI for the key to write
|
||||
// into the generated playlist. Implies "-hls_enc 1".
|
||||
["-hls_key_info_file", keyInfoPath],
|
||||
// Generate as many playlist entries as needed (default limit is 5).
|
||||
["-hls_list_size", "0"],
|
||||
// Place all the video segments within the same .ts file (with the same
|
||||
// path as the playlist file but with a ".ts" extension).
|
||||
["-hls_flags", "single_file"],
|
||||
// Output path where the playlist should be generated.
|
||||
playlistPath,
|
||||
].flat();
|
||||
|
||||
let dimensions: ReturnType<typeof detectVideoDimensions>;
|
||||
let videoSize: number;
|
||||
|
||||
try {
|
||||
// Write the key and the keyInfo to their desired paths.
|
||||
await Promise.all([
|
||||
fs.writeFile(keyPath, keyBytes),
|
||||
fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }),
|
||||
]);
|
||||
|
||||
// Run the ffmpeg command to generate the HLS playlist and segments.
|
||||
//
|
||||
// Note: Depending on the size of the input file, this may take long!
|
||||
const { stderr: conversionStderr } = await execAsync(command);
|
||||
|
||||
// Determine the dimensions of the generated video from the stderr
|
||||
// output produced by ffmpeg during the conversion.
|
||||
dimensions = detectVideoDimensions(conversionStderr);
|
||||
|
||||
// Find the size of the generated video segments by reading the size of
|
||||
// the generated .ts file.
|
||||
videoSize = await fs.stat(videoPath).then((st) => st.size);
|
||||
} catch (e) {
|
||||
log.error("HLS generation failed", e);
|
||||
await Promise.all([
|
||||
deleteTempFileIgnoringErrors(playlistPath),
|
||||
deleteTempFileIgnoringErrors(videoPath),
|
||||
]);
|
||||
throw e;
|
||||
} finally {
|
||||
await Promise.all([
|
||||
deleteTempFileIgnoringErrors(keyInfoPath),
|
||||
deleteTempFileIgnoringErrors(keyPath),
|
||||
// ffmpeg writes a /path/output.ts.tmp, clear it out too.
|
||||
deleteTempFileIgnoringErrors(videoPath + ".tmp"),
|
||||
]);
|
||||
}
|
||||
|
||||
return { playlistPath, videoPath, dimensions, videoSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* A regex that matches the first line of the form
|
||||
*
|
||||
* Stream #0:0: Video: h264 (High 10) ([27][0][0][0] / 0x001B), yuv420p10le(tv, bt2020nc/bt2020/arib-std-b67), 1920x1080, 30 fps, 30 tbr, 90k tbn
|
||||
*
|
||||
* The part after Video: is the first capture group.
|
||||
*
|
||||
* Another example:
|
||||
*
|
||||
* Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)
|
||||
*/
|
||||
const videoStreamLineRegex = /Stream #.+: Video:(.+)\n/;
|
||||
|
||||
/** {@link videoStreamLineRegex}, but global. */
|
||||
const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g;
|
||||
|
||||
/**
|
||||
* A regex that matches "<digits> kb/s" preceded by a space. See
|
||||
* {@link videoStreamLineRegex} for the context in which it is used.
|
||||
*/
|
||||
const videoBitrateRegex = / ([1-9]\d*) kb\/s/;
|
||||
|
||||
/**
|
||||
* A regex that matches <digits>x<digits> pair preceded by a space. See
|
||||
* {@link videoStreamLineRegex} for the context in which it is used.
|
||||
*
|
||||
* We constrain the digit sequence not to begin with 0 to exclude hexadecimal
|
||||
* representations of various constants that ffmpeg prints on this line (e.g.
|
||||
* "avc1 / 0x31637661").
|
||||
*/
|
||||
const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/;
|
||||
|
||||
interface VideoCharacteristics {
|
||||
isH264: boolean;
|
||||
isBT709: boolean;
|
||||
bitrate: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Heuristically determine information about the video at the given
|
||||
* {@link inputFilePath}:
|
||||
*
|
||||
* - If is encoded using H.264 codec.
|
||||
* - If it uses the BT.709 colorspace.
|
||||
* - Its bitrate.
|
||||
*
|
||||
* The defaults are tailored for the cases in which these conditions are used,
|
||||
* so that even if we get the detection wrong we'll only end up encoding videos
|
||||
* that could've possibly been skipped as an optimization.
|
||||
*
|
||||
* [Note: Parsing CLI output might break on ffmpeg updates]
|
||||
*
|
||||
* This function tries to determine the these bits of information about the
|
||||
* given video by scanning the ffmpeg info output for the video stream line, and
|
||||
* doing various string matches and regex extractions.
|
||||
*
|
||||
* Needless to say, while this works currently, this is liable to break in the
|
||||
* future. So if something stops working after updating ffmpeg, look here!
|
||||
*
|
||||
* Ideally, we'd have done this using `ffprobe`, but we don't have the ffprobe
|
||||
* binary at hand, so we make do by grepping the log output of ffmpeg.
|
||||
*
|
||||
* For reference,
|
||||
*
|
||||
* - codec and colorspace are printed by the `avcodec_string` function in the
|
||||
* ffmpeg source:
|
||||
* https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.c
|
||||
*
|
||||
* - bitrate is printed by the `dump_stream_format` function in `dump.c`.
|
||||
*/
|
||||
const detectVideoCharacteristics = async (inputFilePath: string) => {
|
||||
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
|
||||
const videoStreamLine = videoStreamLineRegex.exec(videoInfo)?.at(1)?.trim();
|
||||
|
||||
// Since the checks are heuristic, start with defaults that would cause the
|
||||
// codec conversion to happen, even if it is unnecessary.
|
||||
const res: VideoCharacteristics = {
|
||||
isH264: false,
|
||||
isBT709: false,
|
||||
bitrate: undefined,
|
||||
};
|
||||
if (!videoStreamLine) return res;
|
||||
|
||||
res.isH264 = videoStreamLine.startsWith("h264 ");
|
||||
res.isBT709 = videoStreamLine.includes("bt709");
|
||||
// The regex matches "\d kb/s", but there can be other units for the
|
||||
// bitrate. However, (a) "kb/s" is the most common for videos out in the
|
||||
// wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead
|
||||
// of "-v:c copy", so only unnecessary processing but no change in output.
|
||||
const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0);
|
||||
if (brs) {
|
||||
const br = parseInt(brs, 10);
|
||||
if (br) res.bitrate = br;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Heuristically detect the dimensions of the given video from the log output of
|
||||
* the ffmpeg invocation during the HLS playlist generation.
|
||||
*
|
||||
* This function tries to determine the width and height of the generated video
|
||||
* from the output log written by ffmpeg on its stderr during the generation
|
||||
* process, scanning it for the last video stream line, and trying to match a
|
||||
* "<digits>x<digits>" regex.
|
||||
*
|
||||
* See: [Note: Parsing CLI output might break on ffmpeg updates].
|
||||
*/
|
||||
const detectVideoDimensions = (conversionStderr: string) => {
|
||||
// There is a nicer way to do it - by running `pseudoFFProbeVideo` on the
|
||||
// generated playlist. However, that playlist includes a data URL that
|
||||
// specifies the encryption info, and ffmpeg refuses to read that unless we
|
||||
// specify the "-allowed_extensions ALL" or something to that effect.
|
||||
//
|
||||
// Unfortunately, our current ffmpeg binary (5.x) does not support that
|
||||
// option. So we instead parse the conversion output itself.
|
||||
//
|
||||
// This is also nice, since it saves on an extra ffmpeg invocation. But we
|
||||
// now need to be careful to find the right video stream line, since the
|
||||
// conversion output includes both the input and output video stream lines.
|
||||
//
|
||||
// To match the right (output) video stream line, we use a global regex, and
|
||||
// use the last match since that'd correspond to the single video stream
|
||||
// written in the output.
|
||||
const videoStreamLine = Array.from(
|
||||
conversionStderr.matchAll(videoStreamLinesRegex),
|
||||
)
|
||||
.at(-1) /* Last Stream...: Video: line in the output */
|
||||
?.at(1); /* First capture group */
|
||||
if (videoStreamLine) {
|
||||
const [, ws, hs] = videoDimensionsRegex.exec(videoStreamLine) ?? [];
|
||||
if (ws && hs) {
|
||||
const w = parseInt(ws, 10);
|
||||
const h = parseInt(hs, 10);
|
||||
if (w && h) {
|
||||
return { width: w, height: h };
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to detect video dimensions from stream line [${videoStreamLine ?? ""}]`,
|
||||
export const ffmpegDetermineVideoDuration = async (
|
||||
pathOrZipItem: string | ZipItem,
|
||||
): Promise<number> =>
|
||||
withInputFile(pathOrZipItem, async (worker, inputFilePath) =>
|
||||
worker.ffmpegDetermineVideoDuration(inputFilePath),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Heuristically detect if the file at given path is a HDR video.
|
||||
*
|
||||
* This is similar to {@link detectVideoCharacteristics}, and see that
|
||||
* function's documentation for all the caveats. However, this function uses an
|
||||
* allow-list instead, and considers any file with color transfer "smpte2084" or
|
||||
* "arib-std-b67" to be HDR. While this is in some sense a more exact check, it
|
||||
* comes with different caveats:
|
||||
*
|
||||
* - These particular constants are not guaranteed to be correct; these are just
|
||||
* what I saw on the internet as being used / recommended for detecting HDR.
|
||||
*
|
||||
* - Since we don't have ffprobe, we're not checking the color space value
|
||||
* itself but a substring of the stream line in the ffmpeg stderr output.
|
||||
*
|
||||
* In particular, we use this more exact check for places where we have less
|
||||
* leeway. e.g. when generating thumbnails, if we apply the tonemapping to any
|
||||
* non-BT.709 file (as the HLS stream generation does), we start getting the
|
||||
* "code 3074: no path between colorspaces" error during the JPEG conversion
|
||||
* (this is not a problem in the H.264 conversion).
|
||||
*
|
||||
* - See: [Note: Alternative FFmpeg command for HDR videos]
|
||||
* - See: [Note: Tonemapping HDR to HD]
|
||||
*
|
||||
* @param inputFilePath The path to a video file on the user's machine.
|
||||
*
|
||||
* @returns `true` if this file is likely a HDR video. Exceptions are treated as
|
||||
* `false` to make this function safe to invoke without breaking the happy path.
|
||||
*/
|
||||
const isHDRVideo = async (inputFilePath: string) => {
|
||||
try {
|
||||
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
|
||||
const vs = videoStreamLineRegex.exec(videoInfo)?.at(1);
|
||||
if (!vs) return false;
|
||||
return vs.includes("smpte2084") || vs.includes("arib-std-b67");
|
||||
} catch (e) {
|
||||
log.warn(`Could not detect HDR status of ${inputFilePath}`, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the stderr of ffmpeg in an attempt to gain information about the video
|
||||
* at the given {@link inputFilePath}.
|
||||
*
|
||||
* We don't have the ffprobe binary at hand, which is why we need to use this
|
||||
* alternative. See: [Note: Parsing CLI output might break on ffmpeg updates]
|
||||
*
|
||||
* @returns the stderr of ffmpeg after running it on the input file. The exact
|
||||
* command we run is:
|
||||
*
|
||||
* ffmpeg -i in.mov -an -frames:v 0 -f null - 2>info.txt
|
||||
*
|
||||
* And the returned string is the contents of the `info.txt` thus produced.
|
||||
*/
|
||||
const pseudoFFProbeVideo = async (inputFilePath: string) => {
|
||||
const command = [
|
||||
ffmpegPathPlaceholder,
|
||||
// Reduce the amount of output lines we have to parse.
|
||||
["-hide_banner"],
|
||||
["-i", inputPathPlaceholder],
|
||||
"-an",
|
||||
["-frames:v", "0"],
|
||||
["-f", "null"],
|
||||
"-",
|
||||
].flat();
|
||||
|
||||
const cmd = substitutePlaceholders(command, inputFilePath, /* NA */ "");
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
|
||||
return stderr;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type ZipItem } from "../../types/ipc";
|
||||
import { execAsync, isDev } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeFileForStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
@@ -61,7 +61,7 @@ const vipsPath = () =>
|
||||
);
|
||||
|
||||
export const generateImageThumbnail = async (
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
): Promise<Uint8Array> => {
|
||||
@@ -69,7 +69,7 @@ export const generateImageThumbnail = async (
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
} = await makeFileForStreamOrPathOrZipItem(pathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath("jpeg");
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import log from "../log";
|
||||
import { clearPendingVideoResults } from "../stream";
|
||||
import { clearStores } from "./store";
|
||||
import { watchReset } from "./watch";
|
||||
import { terminateUtilityProcesses } from "./workers";
|
||||
import { clearOpenZipCache } from "./zip";
|
||||
|
||||
/**
|
||||
@@ -36,4 +37,9 @@ export const logout = (watcher: FSWatcher) => {
|
||||
} catch (e) {
|
||||
ignoreError("zip cache", e);
|
||||
}
|
||||
try {
|
||||
terminateUtilityProcesses();
|
||||
} catch (e) {
|
||||
ignoreError("utility processes", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,47 +15,16 @@ import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as ort from "onnxruntime-node";
|
||||
import { z } from "zod";
|
||||
import log from "../log-worker";
|
||||
import { messagePortMainEndpoint } from "../utils/comlink";
|
||||
import { wait } from "../utils/common";
|
||||
import { writeStream } from "../utils/stream";
|
||||
import { fsStatMtime } from "./fs";
|
||||
|
||||
/**
|
||||
* We cannot do
|
||||
*
|
||||
* import log from "../log";
|
||||
*
|
||||
* because that requires the Electron APIs that are not available to a utility
|
||||
* process (See: [Note: Using Electron APIs in UtilityProcess]). But even if
|
||||
* that were to work, logging will still be problematic since we'd try opening
|
||||
* the log file from two different Node.js processes (this one, and the main
|
||||
* one), and I didn't find any indication in the electron-log repository that
|
||||
* the log file's integrity would be maintained in such cases.
|
||||
*
|
||||
* So instead we create this proxy log object that uses `process.parentPort` to
|
||||
* transport the logs over to the main process.
|
||||
*/
|
||||
const log = {
|
||||
/**
|
||||
* Unlike the real {@link log.error}, this accepts only the first string
|
||||
* argument, not the second optional error one.
|
||||
*/
|
||||
errorString: (s: string) => mainProcess("log.errorString", s),
|
||||
info: (...ms: unknown[]) => mainProcess("log.info", ms),
|
||||
/**
|
||||
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
|
||||
* accepts only strings.
|
||||
*/
|
||||
debugString: (s: string) => mainProcess("log.debugString", s),
|
||||
};
|
||||
log.debugString("Started ML utility process");
|
||||
|
||||
/**
|
||||
* Send a message to the main process using a barebones RPC protocol.
|
||||
*/
|
||||
const mainProcess = (method: string, param: unknown) =>
|
||||
process.parentPort.postMessage({ method, p: param });
|
||||
|
||||
log.debugString(`Started ML worker process`);
|
||||
process.on("uncaughtException", (e, origin) => log.error(origin, e));
|
||||
|
||||
process.parentPort.once("message", (e) => {
|
||||
// Initialize ourselves with the data we got from our parent.
|
||||
@@ -84,17 +53,10 @@ let _userDataPath: string | undefined;
|
||||
/** Equivalent to app.getPath("userData") */
|
||||
const userDataPath = () => _userDataPath!;
|
||||
|
||||
const MLWorkerInitData = z.object({ userDataPath: z.string() });
|
||||
|
||||
const parseInitData = (data: unknown) => {
|
||||
if (
|
||||
data &&
|
||||
typeof data == "object" &&
|
||||
"userDataPath" in data &&
|
||||
typeof data.userDataPath == "string"
|
||||
) {
|
||||
_userDataPath = data.userDataPath;
|
||||
} else {
|
||||
log.errorString("Unparseable initialization data");
|
||||
}
|
||||
_userDataPath = MLWorkerInitData.parse(data).userDataPath;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,7 +123,7 @@ const modelPathDownloadingIfNeeded = async (
|
||||
} else {
|
||||
const size = (await fs.stat(modelPath)).size;
|
||||
if (size !== expectedByteSize) {
|
||||
log.errorString(
|
||||
log.error(
|
||||
`The size ${size} of model ${modelName} does not match the expected size, downloading again`,
|
||||
);
|
||||
await downloadModel(modelPath, modelName);
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/**
|
||||
* @file ML related functionality. This code runs in the main process.
|
||||
*/
|
||||
|
||||
import {
|
||||
MessageChannelMain,
|
||||
type BrowserWindow,
|
||||
type UtilityProcess,
|
||||
} from "electron";
|
||||
import { app, utilityProcess } from "electron/main";
|
||||
import path from "node:path";
|
||||
import log from "../log";
|
||||
|
||||
/** The active ML worker (utility) process, if any. */
|
||||
let _child: UtilityProcess | undefined;
|
||||
|
||||
/**
|
||||
* Create a new ML worker process, terminating the older ones (if any).
|
||||
*
|
||||
* [Note: ML IPC]
|
||||
*
|
||||
* The primary reason for doing ML tasks in the Node.js layer is so that we can
|
||||
* use the binary ONNX runtime, which is 10-20x faster than the Wasm one that
|
||||
* can be used directly on the web layer.
|
||||
*
|
||||
* For this to work, the main and renderer process need to communicate with each
|
||||
* other. Further, in the web layer the ML indexing runs in a web worker (so as
|
||||
* to not get in the way of the main thread). So the communication has 2 hops:
|
||||
*
|
||||
* Node.js main <-> Renderer main <-> Renderer web worker
|
||||
*
|
||||
* This naive way works, but has a problem. The Node.js main process is in the
|
||||
* code path for delivering user events to the renderer process. The ML tasks we
|
||||
* do take in the order of 100-300 ms (possibly more) for each individual
|
||||
* inference. Thus, the Node.js main process is busy for those 100-300 ms, and
|
||||
* does not forward events to the renderer, causing the UI to jitter.
|
||||
*
|
||||
* The solution for this is to spawn an Electron UtilityProcess, which we can
|
||||
* think of a regular Node.js child process. This frees up the Node.js main
|
||||
* process, and would remove the jitter.
|
||||
* https://www.electronjs.org/docs/latest/tutorial/process-model
|
||||
*
|
||||
* It would seem that this introduces another hop in our IPC
|
||||
*
|
||||
* Node.js utility process <-> Node.js main <-> ...
|
||||
*
|
||||
* but here we can use the special bit about Electron utility processes that
|
||||
* separates them from regular Node.js child processes: their support for
|
||||
* message ports. https://www.electronjs.org/docs/latest/tutorial/message-ports
|
||||
*
|
||||
* As a brief summary, a MessagePort is a web feature that allows two contexts
|
||||
* to communicate. A pair of message ports is called a message channel. The cool
|
||||
* thing about these is that we can pass these ports themselves over IPC.
|
||||
*
|
||||
* > One caveat here is that the message ports can only be passed using the
|
||||
* > `postMessage` APIs, not the usual send/invoke APIs.
|
||||
*
|
||||
* So we
|
||||
*
|
||||
* 1. In the utility process create a message channel.
|
||||
* 2. Spawn a utility process, and send one port of the pair to it.
|
||||
* 3. Send the other port of the pair to the renderer.
|
||||
*
|
||||
* The renderer will forward that port to the web worker that is coordinating
|
||||
* the ML indexing on the web layer. Thereafter, the utility process and web
|
||||
* worker can directly talk to each other!
|
||||
*
|
||||
* Node.js utility process <-> Renderer web worker
|
||||
*
|
||||
* The RPC protocol is handled using comlink on both ends. The port itself needs
|
||||
* to be relayed using `postMessage`.
|
||||
*/
|
||||
export const createMLWorker = (window: BrowserWindow) => {
|
||||
if (_child) {
|
||||
log.debug(() => "Terminating previous ML worker process");
|
||||
_child.kill();
|
||||
_child = undefined;
|
||||
}
|
||||
|
||||
const { port1, port2 } = new MessageChannelMain();
|
||||
|
||||
const child = utilityProcess.fork(path.join(__dirname, "ml-worker.js"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
child.postMessage({ userDataPath }, [port1]);
|
||||
|
||||
window.webContents.postMessage("createMLWorker/port", undefined, [port2]);
|
||||
|
||||
handleMessagesFromUtilityProcess(child);
|
||||
|
||||
_child = child;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle messages posted from the utility process.
|
||||
*
|
||||
* [Note: Using Electron APIs in UtilityProcess]
|
||||
*
|
||||
* Only a small subset of the Electron APIs are available to a UtilityProcess.
|
||||
* As of writing (Jul 2024, Electron 30), only the following are available:
|
||||
*
|
||||
* - net
|
||||
* - systemPreferences
|
||||
*
|
||||
* In particular, `app` is not available.
|
||||
*
|
||||
* We structure our code so that it doesn't need anything apart from `net`.
|
||||
*
|
||||
* For the other cases,
|
||||
*
|
||||
* - Additional parameters to the utility process are passed alongwith the
|
||||
* initial message where we provide it the message port.
|
||||
*
|
||||
* - When we need to communicate from the utility process to the main process,
|
||||
* we use the `parentPort` in the utility process.
|
||||
*/
|
||||
const handleMessagesFromUtilityProcess = (child: UtilityProcess) => {
|
||||
const logTag = "[ml-worker]";
|
||||
child.on("message", (m: unknown) => {
|
||||
if (m && typeof m == "object" && "method" in m && "p" in m) {
|
||||
const p = m.p;
|
||||
switch (m.method) {
|
||||
case "log.errorString":
|
||||
if (typeof p == "string") {
|
||||
log.error(`${logTag} ${p}`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "log.info":
|
||||
if (Array.isArray(p)) {
|
||||
// Need to cast from any[] to unknown[]
|
||||
log.info(logTag, ...(p as unknown[]));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "log.debugString":
|
||||
if (typeof p == "string") {
|
||||
log.debug(() => `${logTag} ${p}`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.info("Ignoring unknown message from ML worker", m);
|
||||
});
|
||||
};
|
||||
241
desktop/src/main/services/workers.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @file This main process code and interface for dealing with the various
|
||||
* utility processes that we create.
|
||||
*/
|
||||
|
||||
import type { Endpoint } from "comlink";
|
||||
import {
|
||||
MessageChannelMain,
|
||||
type BrowserWindow,
|
||||
type UtilityProcess,
|
||||
} from "electron";
|
||||
import { app, utilityProcess } from "electron/main";
|
||||
import path from "node:path";
|
||||
import type { UtilityProcessType } from "../../types/ipc";
|
||||
import log, { processUtilityProcessLogMessage } from "../log";
|
||||
import { messagePortMainEndpoint } from "../utils/comlink";
|
||||
|
||||
/**
|
||||
* Terminate any existing utility processes if they're running.
|
||||
*
|
||||
* This function is called during the logout sequence.
|
||||
*/
|
||||
export const terminateUtilityProcesses = () => {
|
||||
terminateMLProcessIfRunning();
|
||||
terminateFFmpegProcessIfRunning();
|
||||
};
|
||||
|
||||
/** The active ML utility process, if any. */
|
||||
let _utilityProcessML: UtilityProcess | undefined;
|
||||
|
||||
/** The active FFmpeg utility process, if any. */
|
||||
let _utilityProcessFFmpeg: UtilityProcess | undefined;
|
||||
|
||||
/**
|
||||
* A promise to a comlink {@link Endpoint} that can be used to communicate with
|
||||
* the active ffmpeg utility process (if any).
|
||||
*/
|
||||
let _utilityProcessFFmpegEndpoint: Promise<Endpoint> | undefined;
|
||||
|
||||
/**
|
||||
* Create a new utility process of the given {@link type}, terminating the older
|
||||
* ones (if any).
|
||||
*
|
||||
* Currently the only type is "ml". The following note explains the reasoning
|
||||
* why utility processes were used for the first workload (ML) that was handled
|
||||
* this way. Similar reasoning applies to subsequent workloads (ffmpeg) that
|
||||
* have been offloaded to utility processes in a slightly different manner to
|
||||
* avoid stutter in the UI.
|
||||
*
|
||||
* [Note: ML IPC]
|
||||
*
|
||||
* The primary reason for doing ML tasks in the Node.js layer is so that we can
|
||||
* use the binary ONNX runtime, which is 10-20x faster than the Wasm one that
|
||||
* can be used directly on the web layer.
|
||||
*
|
||||
* For this to work, the main and renderer process need to communicate with each
|
||||
* other. Further, in the web layer the ML indexing runs in a web worker (so as
|
||||
* to not get in the way of the main thread). So the communication has 2 hops:
|
||||
*
|
||||
* Node.js main <-> Renderer main <-> Renderer web worker
|
||||
*
|
||||
* This naive way works, but has a problem. The Node.js main process is in the
|
||||
* code path for delivering user events to the renderer process. The ML tasks we
|
||||
* do take in the order of 100-300 ms (possibly more) for each individual
|
||||
* inference. Thus, the Node.js main process is busy for those 100-300 ms, and
|
||||
* does not forward events to the renderer, causing the UI to jitter.
|
||||
*
|
||||
* The solution for this is to spawn an Electron UtilityProcess, which we can
|
||||
* think of a regular Node.js child process. This frees up the Node.js main
|
||||
* process, and would remove the jitter.
|
||||
* https://www.electronjs.org/docs/latest/tutorial/process-model
|
||||
*
|
||||
* It would seem that this introduces another hop in our IPC
|
||||
*
|
||||
* Node.js utility process <-> Node.js main <-> ...
|
||||
*
|
||||
* but here we can use the special bit about Electron utility processes that
|
||||
* separates them from regular Node.js child processes: their support for
|
||||
* message ports. https://www.electronjs.org/docs/latest/tutorial/message-ports
|
||||
*
|
||||
* As a brief summary, a MessagePort is a web feature that allows two contexts
|
||||
* to communicate. A pair of message ports is called a message channel. The cool
|
||||
* thing about these is that we can pass these ports themselves over IPC.
|
||||
*
|
||||
* > One caveat here is that the message ports can only be passed using the
|
||||
* > `postMessage` APIs, not the usual send/invoke APIs.
|
||||
*
|
||||
* So we
|
||||
*
|
||||
* 1. In the utility process create a message channel.
|
||||
* 2. Spawn a utility process, and send one port of the pair to it.
|
||||
* 3. Send the other port of the pair to the renderer.
|
||||
*
|
||||
* The renderer will forward that port to the web worker that is coordinating
|
||||
* the ML indexing on the web layer. Thereafter, the utility process and web
|
||||
* worker can directly talk to each other!
|
||||
*
|
||||
* Node.js utility process <-> Renderer web worker
|
||||
*
|
||||
* The RPC protocol is handled using comlink on both ends. The port itself needs
|
||||
* to be relayed using `postMessage`.
|
||||
*/
|
||||
export const triggerCreateUtilityProcess = (
|
||||
type: UtilityProcessType,
|
||||
window: BrowserWindow,
|
||||
) => triggerCreateMLUtilityProcess(window);
|
||||
|
||||
const terminateMLProcessIfRunning = () => {
|
||||
if (_utilityProcessML) {
|
||||
log.debug(() => "Terminating running ML utility process");
|
||||
_utilityProcessML.kill();
|
||||
_utilityProcessML = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => {
|
||||
terminateMLProcessIfRunning();
|
||||
|
||||
const { port1, port2 } = new MessageChannelMain();
|
||||
|
||||
const child = utilityProcess.fork(path.join(__dirname, "ml-worker.js"));
|
||||
const userDataPath = app.getPath("userData");
|
||||
child.postMessage(/* MLWorkerInitData */ { userDataPath }, [port1]);
|
||||
|
||||
window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]);
|
||||
|
||||
handleMessagesFromMLUtilityProcess(child);
|
||||
|
||||
_utilityProcessML = child;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle messages posted from the utility process.
|
||||
*
|
||||
* [Note: Using Electron APIs in UtilityProcess]
|
||||
*
|
||||
* Only a small subset of the Electron APIs are available to a UtilityProcess.
|
||||
* As of writing (Jul 2024, Electron 30), only the following are available:
|
||||
*
|
||||
* - net
|
||||
* - systemPreferences
|
||||
*
|
||||
* In particular, `app` is not available.
|
||||
*
|
||||
* We structure our code so that it doesn't need anything apart from `net`.
|
||||
*
|
||||
* For the other cases,
|
||||
*
|
||||
* - Additional parameters to the utility process are passed alongwith the
|
||||
* initial message where we provide it the message port.
|
||||
*
|
||||
* - When we need to communicate from the utility process to the main process,
|
||||
* we use the `parentPort` in the utility process.
|
||||
*/
|
||||
const handleMessagesFromMLUtilityProcess = (child: UtilityProcess) => {
|
||||
child.on("message", (m: unknown) => {
|
||||
if (processUtilityProcessLogMessage("[ml-worker]", m)) {
|
||||
return;
|
||||
}
|
||||
log.info("Ignoring unknown message from ML utility process", m);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A comlink endpoint that can be used to communicate with the ffmpeg utility
|
||||
* process. If there is no ffmpeg utility process, a new one is created on
|
||||
* demand.
|
||||
*
|
||||
* See [Note: ML IPC] for a general outline of why utility processes are needed
|
||||
* (tl;dr; to avoid stutter on the UI).
|
||||
*
|
||||
* In the case of ffmpeg, the IPC flow is a bit different: the utility process
|
||||
* is not exposed to the web layer, and is internal to the node layer. The
|
||||
* reason for this difference is that we need to create temporary files etc, and
|
||||
* doing it a utility process requires access to the `app` module which are not
|
||||
* accessible (See: [Note: Using Electron APIs in UtilityProcess]).
|
||||
*
|
||||
* There could've been possible reasonable workarounds, but the architecture
|
||||
* we've adopted of three layers:
|
||||
*
|
||||
* Renderer (web) <-> Node.js main <-> Node.js ffmpeg utility process
|
||||
*
|
||||
* The temporary file creation etc is handled in the Node.js main process, and
|
||||
* paths to the files are forwarded to the ffmpeg utility process to act on.
|
||||
*
|
||||
* @returns an endpoint that can be used to communicate with the utility
|
||||
* process. The utility process is expected to expose an object that conforms to
|
||||
* the {@link ElectronFFmpegWorkerNode} interface on this endpoint.
|
||||
*/
|
||||
export const ffmpegUtilityProcessEndpoint = () =>
|
||||
(_utilityProcessFFmpegEndpoint ??= createFFmpegUtilityProcessEndpoint());
|
||||
|
||||
const terminateFFmpegProcessIfRunning = () => {
|
||||
if (_utilityProcessFFmpeg) {
|
||||
log.debug(() => "Terminating running FFmpeg utility process");
|
||||
_utilityProcessFFmpeg.kill();
|
||||
_utilityProcessFFmpeg = undefined;
|
||||
_utilityProcessFFmpegEndpoint = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const createFFmpegUtilityProcessEndpoint = () => {
|
||||
if (_utilityProcessFFmpeg) {
|
||||
throw new Error("FFmpeg utility process is already running");
|
||||
}
|
||||
|
||||
// Promise.withResolvers is currently in the node available to us.
|
||||
let resolve: ((endpoint: Endpoint) => void) | undefined;
|
||||
const promise = new Promise<Endpoint>((r) => (resolve = r));
|
||||
|
||||
const { port1, port2 } = new MessageChannelMain();
|
||||
|
||||
const child = utilityProcess.fork(path.join(__dirname, "ffmpeg-worker.js"));
|
||||
// Send a handle to the port (one end of the message channel) to the utility
|
||||
// process (alongwith any other init data). The utility process will reply
|
||||
// with an "ack" when it get it.
|
||||
const appVersion = app.getVersion();
|
||||
child.postMessage(/* FFmpegWorkerInitData */ { appVersion }, [port1]);
|
||||
|
||||
child.on("message", (m: unknown) => {
|
||||
if (m && typeof m == "object" && "method" in m) {
|
||||
switch (m.method) {
|
||||
case "ack":
|
||||
resolve!(messagePortMainEndpoint(port2));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (processUtilityProcessLogMessage("[ffmpeg-worker]", m)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Ignoring unknown message from ffmpeg utility process", m);
|
||||
});
|
||||
|
||||
_utilityProcessFFmpeg = child;
|
||||
|
||||
// Resolve with the other end of the message channel (once we get an "ack"
|
||||
// from the utility process).
|
||||
return promise;
|
||||
};
|
||||
@@ -3,23 +3,18 @@
|
||||
*/
|
||||
import { net, protocol } from "electron/main";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs_ from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { Writable } from "node:stream";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import log from "./log";
|
||||
import {
|
||||
ffmpegConvertToMP4,
|
||||
ffmpegGenerateHLSPlaylistAndSegments,
|
||||
type FFmpegGenerateHLSPlaylistAndSegmentsResult,
|
||||
} from "./services/ffmpeg";
|
||||
import { ffmpegUtilityProcess } from "./services/ffmpeg";
|
||||
import { type FFmpegGenerateHLSPlaylistAndSegmentsResult } from "./services/ffmpeg-worker";
|
||||
import { markClosableZip, openZip } from "./services/zip";
|
||||
import { wait } from "./utils/common";
|
||||
import { writeStream } from "./utils/stream";
|
||||
import {
|
||||
deleteTempFile,
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeFileForStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "./utils/temp";
|
||||
|
||||
@@ -234,12 +229,14 @@ export const clearPendingVideoResults = () => pendingVideoResults.clear();
|
||||
* See also: [Note: IPC streams]
|
||||
*/
|
||||
const handleConvertToMP4Write = async (request: Request) => {
|
||||
const worker = await ffmpegUtilityProcess();
|
||||
|
||||
const inputTempFilePath = await makeTempFilePath();
|
||||
await writeStream(inputTempFilePath, request.body!);
|
||||
|
||||
const outputTempFilePath = await makeTempFilePath("mp4");
|
||||
try {
|
||||
await ffmpegConvertToMP4(inputTempFilePath, outputTempFilePath);
|
||||
await worker.ffmpegConvertToMP4(inputTempFilePath, outputTempFilePath);
|
||||
} catch (e) {
|
||||
log.error("Conversion to MP4 failed", e);
|
||||
await deleteTempFileIgnoringErrors(outputTempFilePath);
|
||||
@@ -280,9 +277,9 @@ const handleVideoDone = async (token: string) => {
|
||||
*
|
||||
* The difference here is that we the conversion generates two streams^ - one
|
||||
* for the HLS playlist itself, and one for the file containing the encrypted
|
||||
* and transcoded video chunks. The video stream we write to the objectUploadURL
|
||||
* (provided via {@link params}), and then we return a JSON object containing
|
||||
* the token for the playlist, and other metadata for use by the renderer.
|
||||
* and transcoded video chunks. The video stream we write to the pre-signed
|
||||
* object upload URL(s), and then we return a JSON object containing the token
|
||||
* for the playlist, and other metadata for use by the renderer.
|
||||
*
|
||||
* ^ if the video doesn't require a stream to be generated (e.g. it is very
|
||||
* small and already uses a compatible codec) then a HTT 204 is returned and
|
||||
@@ -292,10 +289,12 @@ const handleGenerateHLSWrite = async (
|
||||
request: Request,
|
||||
params: URLSearchParams,
|
||||
) => {
|
||||
const objectUploadURL = params.get("objectUploadURL");
|
||||
if (!objectUploadURL) throw new Error("Missing objectUploadURL");
|
||||
const fileID = parseInt(params.get("fileID") ?? "", 10);
|
||||
const fetchURL = params.get("fetchURL");
|
||||
const authToken = params.get("authToken");
|
||||
if (!fileID || !fetchURL || !authToken) throw new Error("Missing params");
|
||||
|
||||
let inputItem: Parameters<typeof makeFileForDataOrStreamOrPathOrZipItem>[0];
|
||||
let inputItem: Parameters<typeof makeFileForStreamOrPathOrZipItem>[0];
|
||||
const path = params.get("path");
|
||||
if (path) {
|
||||
inputItem = path;
|
||||
@@ -311,20 +310,25 @@ const handleGenerateHLSWrite = async (
|
||||
}
|
||||
}
|
||||
|
||||
const worker = await ffmpegUtilityProcess();
|
||||
|
||||
const {
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(inputItem);
|
||||
} = await makeFileForStreamOrPathOrZipItem(inputItem);
|
||||
|
||||
const outputFilePathPrefix = await makeTempFilePath();
|
||||
let result: FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined;
|
||||
try {
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
result = await ffmpegGenerateHLSPlaylistAndSegments(
|
||||
result = await worker.ffmpegGenerateHLSPlaylistAndSegments(
|
||||
inputFilePath,
|
||||
outputFilePathPrefix,
|
||||
fileID,
|
||||
fetchURL,
|
||||
authToken,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
@@ -332,94 +336,22 @@ const handleGenerateHLSWrite = async (
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const { playlistPath, videoPath } = result;
|
||||
try {
|
||||
await uploadVideoSegments(videoPath, objectUploadURL);
|
||||
const { playlistPath, dimensions, videoSize, videoObjectID } = result;
|
||||
|
||||
const playlistToken = randomUUID();
|
||||
pendingVideoResults.set(playlistToken, playlistPath);
|
||||
const playlistToken = randomUUID();
|
||||
pendingVideoResults.set(playlistToken, playlistPath);
|
||||
|
||||
const { dimensions, videoSize } = result;
|
||||
return new Response(
|
||||
JSON.stringify({ playlistToken, dimensions, videoSize }),
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (e) {
|
||||
await deleteTempFileIgnoringErrors(playlistPath);
|
||||
throw e;
|
||||
} finally {
|
||||
await deleteTempFileIgnoringErrors(videoPath);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
playlistToken,
|
||||
dimensions,
|
||||
videoSize,
|
||||
videoObjectID,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
} finally {
|
||||
if (isInputFileTemporary)
|
||||
await deleteTempFileIgnoringErrors(inputFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload the file at the given {@link videoFilePath} to the provided presigned
|
||||
* {@link objectUploadURL} using a HTTP PUT request.
|
||||
*
|
||||
* In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff.
|
||||
*
|
||||
* See: [Note: Upload HLS video segment from node side].
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx`
|
||||
* from `web/packages/base/http.ts` (we don't have the rest of the scaffolding
|
||||
* used by that function, which is why it is inlined bespoked).
|
||||
*
|
||||
* It handles the specific use case of uploading videos since generating the HLS
|
||||
* stream is a fairly expensive operation, so a retry to discount transient
|
||||
* network issues is called for. There are only 2 retries for a total of 3
|
||||
* attempts, and the retry gaps are more spaced out.
|
||||
*/
|
||||
export const uploadVideoSegments = async (
|
||||
videoFilePath: string,
|
||||
objectUploadURL: string,
|
||||
) => {
|
||||
const waitTimeBeforeNextTry = [5000, 20000];
|
||||
|
||||
while (true) {
|
||||
let abort = false;
|
||||
try {
|
||||
const nodeStream = fs_.createReadStream(videoFilePath);
|
||||
const webStream = Readable.toWeb(nodeStream);
|
||||
|
||||
const res = await net.fetch(objectUploadURL, {
|
||||
method: "PUT",
|
||||
// The duplex option is required since we're passing a stream.
|
||||
//
|
||||
// @ts-expect-error TypeScript's libdom.d.ts does not include
|
||||
// the "duplex" parameter, e.g. see
|
||||
// https://github.com/node-fetch/node-fetch/issues/1769.
|
||||
duplex: "half",
|
||||
body: webStream,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Success.
|
||||
return;
|
||||
}
|
||||
if (res.status >= 400 && res.status < 500) {
|
||||
// HTTP 4xx.
|
||||
abort = true;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`,
|
||||
);
|
||||
} catch (e) {
|
||||
if (abort) {
|
||||
throw e;
|
||||
}
|
||||
const t = waitTimeBeforeNextTry.shift();
|
||||
if (!t) {
|
||||
throw e;
|
||||
} else {
|
||||
log.warn("Will retry potentially transient request failure", e);
|
||||
}
|
||||
await wait(t);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ export const messagePortMainEndpoint = (mp: MessagePortMain): Endpoint => {
|
||||
const listeners = new WeakMap<NL, EL>();
|
||||
return {
|
||||
postMessage: (message, transfer) => {
|
||||
mp.postMessage(message, transfer as unknown as MessagePortMain[]);
|
||||
mp.postMessage(message, (transfer ?? []) as MessagePortMain[]);
|
||||
},
|
||||
addEventListener: (_, eh) => {
|
||||
const l: EL = (data) =>
|
||||
|
||||
@@ -15,3 +15,11 @@
|
||||
*/
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Convert `null` to `undefined`, passthrough everything else unchanged.
|
||||
*
|
||||
* Duplicated from `web/packages/utils/transform.ts`.
|
||||
*/
|
||||
export const nullToUndefined = <T>(v: T | null | undefined): T | undefined =>
|
||||
v === null ? undefined : v;
|
||||
|
||||
23
desktop/src/main/utils/exec-worker.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import shellescape from "any-shell-escape";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import log from "../log-worker";
|
||||
|
||||
/**
|
||||
* Run a shell command asynchronously (utility process edition).
|
||||
*
|
||||
* This is an almost verbatim copy of {@link execAsync} from `electron.ts`,
|
||||
* except it is meant to be usable from a utility process where only a subset of
|
||||
* imports are available. See [Note: Using Electron APIs in UtilityProcess].
|
||||
*/
|
||||
export const execAsyncWorker = async (command: string | string[]) => {
|
||||
const escapedCommand = Array.isArray(command)
|
||||
? shellescape(command)
|
||||
: command;
|
||||
const startTime = Date.now();
|
||||
const result = await execAsync_(escapedCommand);
|
||||
log.debugString(`${escapedCommand} (${Date.now() - startTime} ms)`);
|
||||
return result;
|
||||
};
|
||||
|
||||
const execAsync_ = promisify(exec);
|
||||
31
desktop/src/main/utils/http.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const clientPackageName = "io.ente.photos.desktop";
|
||||
|
||||
/**
|
||||
* Reimplementation of {@link publicRequestHeaders} from the web source.
|
||||
*
|
||||
* @param desktopAppVersion The desktop app's version. This will get passed on
|
||||
* as the "X-Client-Version" header.
|
||||
*
|
||||
* We cannot directly use `app.getVersion()` to obtain this value since the
|
||||
* {@link app} module is not accessible to Electron utility processes which also
|
||||
* calls this function.
|
||||
*/
|
||||
export const publicRequestHeaders = (desktopAppVersion: string) => ({
|
||||
"X-Client-Package": clientPackageName,
|
||||
"X-Client-Version": desktopAppVersion,
|
||||
});
|
||||
|
||||
/**
|
||||
* Reimplementation of {@link authenticatedRequestHeaders} from the web source.
|
||||
*
|
||||
* This builds on top of {@link publicRequestHeaders} and takes the same
|
||||
* parameters, and additionally also requires the {@link authToken} that will be
|
||||
* passed as the "X-Auth-Token" header.
|
||||
*/
|
||||
export const authenticatedRequestHeaders = (
|
||||
desktopAppVersion: string,
|
||||
authToken: string,
|
||||
) => ({
|
||||
...publicRequestHeaders(desktopAppVersion),
|
||||
"X-Auth-Token": authToken,
|
||||
});
|
||||
@@ -80,8 +80,8 @@ export const deleteTempFileIgnoringErrors = async (tempFilePath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** The result of {@link makeFileForDataOrStreamOrPathOrZipItem}. */
|
||||
interface FileForDataOrPathOrZipItem {
|
||||
/** The result of {@link makeFileForStreamOrPathOrZipItem}. */
|
||||
interface FileForStreamOrPathOrZipItem {
|
||||
/**
|
||||
* The path to the file (possibly temporary).
|
||||
*/
|
||||
@@ -107,13 +107,13 @@ interface FileForDataOrPathOrZipItem {
|
||||
* that needs to be deleted after processing, and a function to write the given
|
||||
* {@link item} into that temporary file if needed.
|
||||
*
|
||||
* @param item The contents of the file (bytes), or a {@link ReadableStream}
|
||||
* with the contents of the file, or the path to an existing file, or a (path to
|
||||
* a zip file, name of an entry within that zip file) tuple.
|
||||
* @param item A {@link ReadableStream} with the contents of the file, or the
|
||||
* path to an existing file, or a (path to a zip file, name of an entry within
|
||||
* that zip file) tuple.
|
||||
*/
|
||||
export const makeFileForDataOrStreamOrPathOrZipItem = async (
|
||||
item: Uint8Array | ReadableStream | string | ZipItem,
|
||||
): Promise<FileForDataOrPathOrZipItem> => {
|
||||
export const makeFileForStreamOrPathOrZipItem = async (
|
||||
item: ReadableStream | string | ZipItem,
|
||||
): Promise<FileForStreamOrPathOrZipItem> => {
|
||||
let path: string;
|
||||
let isFileTemporary: boolean;
|
||||
let writeToTemporaryFile = async () => {
|
||||
@@ -126,9 +126,7 @@ export const makeFileForDataOrStreamOrPathOrZipItem = async (
|
||||
} else {
|
||||
path = await makeTempFilePath();
|
||||
isFileTemporary = true;
|
||||
if (item instanceof Uint8Array) {
|
||||
writeToTemporaryFile = () => fs.writeFile(path, item);
|
||||
} else if (item instanceof ReadableStream) {
|
||||
if (item instanceof ReadableStream) {
|
||||
writeToTemporaryFile = () => writeStream(path, item);
|
||||
} else {
|
||||
writeToTemporaryFile = async () => {
|
||||
|
||||
@@ -69,6 +69,7 @@ import type {
|
||||
FFmpegCommand,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
UtilityProcessType,
|
||||
ZipItem,
|
||||
} from "./types/ipc";
|
||||
|
||||
@@ -192,41 +193,45 @@ const convertToJPEG = (imageData: Uint8Array) =>
|
||||
ipcRenderer.invoke("convertToJPEG", imageData);
|
||||
|
||||
const generateImageThumbnail = (
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"generateImageThumbnail",
|
||||
dataOrPathOrZipItem,
|
||||
pathOrZipItem,
|
||||
maxDimension,
|
||||
maxSize,
|
||||
);
|
||||
|
||||
const ffmpegExec = (
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
pathOrZipItem: string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
) =>
|
||||
ipcRenderer.invoke(
|
||||
"ffmpegExec",
|
||||
command,
|
||||
dataOrPathOrZipItem,
|
||||
pathOrZipItem,
|
||||
outputFileExtension,
|
||||
);
|
||||
|
||||
// - ML
|
||||
const ffmpegDetermineVideoDuration = (pathOrZipItem: string | ZipItem) =>
|
||||
ipcRenderer.invoke("ffmpegDetermineVideoDuration", pathOrZipItem);
|
||||
|
||||
const createMLWorker = () => {
|
||||
// - Utility processes
|
||||
|
||||
const triggerCreateUtilityProcess = (type: UtilityProcessType) => {
|
||||
const portEvent = `utilityProcessPort/${type}`;
|
||||
const l = (event: IpcRendererEvent) => {
|
||||
void windowLoaded.then(() => {
|
||||
// "*"" is the origin to send to.
|
||||
window.postMessage("createMLWorker/port", "*", event.ports);
|
||||
ipcRenderer.off("createMLWorker/port", l);
|
||||
window.postMessage(portEvent, "*", event.ports);
|
||||
ipcRenderer.off(portEvent, l);
|
||||
});
|
||||
};
|
||||
ipcRenderer.on("createMLWorker/port", l);
|
||||
ipcRenderer.send("createMLWorker");
|
||||
ipcRenderer.on(portEvent, l);
|
||||
ipcRenderer.send("triggerCreateUtilityProcess", type);
|
||||
};
|
||||
|
||||
// - Watch
|
||||
@@ -390,10 +395,11 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
ffmpegExec,
|
||||
ffmpegDetermineVideoDuration,
|
||||
|
||||
// - ML
|
||||
|
||||
createMLWorker,
|
||||
triggerCreateUtilityProcess,
|
||||
|
||||
// - Watch
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
*/
|
||||
|
||||
export type UtilityProcessType = "ml";
|
||||
|
||||
export interface AppUpdate {
|
||||
autoUpdatable: boolean;
|
||||
version: string;
|
||||
|
||||
@@ -136,13 +136,20 @@
|
||||
minimatch "^9.0.3"
|
||||
plist "^3.1.0"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0":
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae"
|
||||
@@ -177,10 +184,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
|
||||
integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
|
||||
|
||||
"@eslint/js@^9.25.1":
|
||||
version "9.25.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117"
|
||||
integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==
|
||||
"@eslint/js@^9.27.0":
|
||||
version "9.27.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0"
|
||||
integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==
|
||||
|
||||
"@eslint/object-schema@^2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -268,10 +275,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@pkgr/core@^0.1.0":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
"@pkgr/core@^0.2.4":
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.4.tgz#d897170a2b0ba51f78a099edccd968f7b103387c"
|
||||
integrity sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==
|
||||
|
||||
"@sindresorhus/is@^4.0.0":
|
||||
version "4.6.0"
|
||||
@@ -290,10 +297,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@tsconfig/node22@^22.0.1":
|
||||
version "22.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.1.tgz#27e3ee9b359e31e5b94690bf2bad5a923c1d57d0"
|
||||
integrity sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg==
|
||||
"@tsconfig/node22@^22.0.2":
|
||||
version "22.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node22/-/node22-22.0.2.tgz#1e04e2c5cc946dac787d69bb502462a851ae51b6"
|
||||
integrity sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==
|
||||
|
||||
"@types/auto-launch@^5.0.5":
|
||||
version "5.0.5"
|
||||
@@ -385,85 +392,85 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a"
|
||||
integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==
|
||||
"@typescript-eslint/eslint-plugin@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz#9185b3eaa3b083d8318910e12d56c68b3c4f45b4"
|
||||
integrity sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/type-utils" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/scope-manager" "8.32.1"
|
||||
"@typescript-eslint/type-utils" "8.32.1"
|
||||
"@typescript-eslint/utils" "8.32.1"
|
||||
"@typescript-eslint/visitor-keys" "8.32.1"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b"
|
||||
integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==
|
||||
"@typescript-eslint/parser@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.32.1.tgz#18b0e53315e0bc22b2619d398ae49a968370935e"
|
||||
integrity sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/scope-manager" "8.32.1"
|
||||
"@typescript-eslint/types" "8.32.1"
|
||||
"@typescript-eslint/typescript-estree" "8.32.1"
|
||||
"@typescript-eslint/visitor-keys" "8.32.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b"
|
||||
integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==
|
||||
"@typescript-eslint/scope-manager@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz#9a6bf5fb2c5380e14fe9d38ccac6e4bbe17e8afc"
|
||||
integrity sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/types" "8.32.1"
|
||||
"@typescript-eslint/visitor-keys" "8.32.1"
|
||||
|
||||
"@typescript-eslint/type-utils@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c"
|
||||
integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==
|
||||
"@typescript-eslint/type-utils@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz#b9292a45f69ecdb7db74d1696e57d1a89514d21e"
|
||||
integrity sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.32.1"
|
||||
"@typescript-eslint/utils" "8.32.1"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.1"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4"
|
||||
integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==
|
||||
"@typescript-eslint/types@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.32.1.tgz#b19fe4ac0dc08317bae0ce9ec1168123576c1d4b"
|
||||
integrity sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf"
|
||||
integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==
|
||||
"@typescript-eslint/typescript-estree@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz#9023720ca4ecf4f59c275a05b5fed69b1276face"
|
||||
integrity sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
"@typescript-eslint/types" "8.32.1"
|
||||
"@typescript-eslint/visitor-keys" "8.32.1"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14"
|
||||
integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==
|
||||
"@typescript-eslint/utils@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.32.1.tgz#4d6d5d29b9e519e9a85e9a74e9f7bdb58abe9704"
|
||||
integrity sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.32.1"
|
||||
"@typescript-eslint/types" "8.32.1"
|
||||
"@typescript-eslint/typescript-estree" "8.32.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75"
|
||||
integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==
|
||||
"@typescript-eslint/visitor-keys@8.32.1":
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz#4321395cc55c2eb46036cbbb03e101994d11ddca"
|
||||
integrity sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/types" "8.32.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@xmldom/xmldom@^0.8.8":
|
||||
@@ -1007,6 +1014,17 @@ cross-env@^7.0.3:
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57"
|
||||
integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
@@ -1087,7 +1105,7 @@ detect-libc@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700"
|
||||
integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
|
||||
|
||||
detect-newline@^4.0.0:
|
||||
detect-newline@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23"
|
||||
integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==
|
||||
@@ -1216,10 +1234,10 @@ electron-updater@^6.6.3:
|
||||
semver "^7.6.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^36.1.0:
|
||||
version "36.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.1.0.tgz#9919b77e61cd1400acc6dd24f9db8451fba5f8eb"
|
||||
integrity sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==
|
||||
electron@^36.3.1:
|
||||
version "36.3.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.1.tgz#12a8c1b1cd9163a4bd0cb60f89816243b26ab788"
|
||||
integrity sha512-LeOZ+tVahmctHaAssLCGRRUa2SAO09GXua3pKdG+WzkbSDMh+3iOPONNVPTqGp8HlWnzGj4r6mhsIbM2RgH+eQ==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^22.7.7"
|
||||
@@ -1289,7 +1307,7 @@ eslint-scope@^8.0.2:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^5.2.0"
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||
@@ -1372,6 +1390,19 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
execa@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
|
||||
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^4.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
exponential-backoff@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6"
|
||||
@@ -1438,10 +1469,10 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fdir@^6.4.2:
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689"
|
||||
integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==
|
||||
fdir@^6.4.4:
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9"
|
||||
integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
|
||||
|
||||
ffmpeg-static@^5.2.0:
|
||||
version "5.2.0"
|
||||
@@ -1589,10 +1620,12 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
|
||||
has-symbols "^1.0.3"
|
||||
hasown "^2.0.0"
|
||||
|
||||
get-stdin@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575"
|
||||
integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==
|
||||
get-stream@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
get-stream@^5.1.0:
|
||||
version "5.2.0"
|
||||
@@ -1601,10 +1634,10 @@ get-stream@^5.1.0:
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
git-hooks-list@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc"
|
||||
integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA==
|
||||
git-hooks-list@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-4.1.1.tgz#ae340b82a9312354c73b48007f33840bbd83d3c0"
|
||||
integrity sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==
|
||||
|
||||
glob-parent@^5.1.2:
|
||||
version "5.1.2"
|
||||
@@ -1632,7 +1665,7 @@ glob@^10.3.12, glob@^10.3.7:
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
glob@^7.0.0, glob@^7.1.3, glob@^7.1.6:
|
||||
glob@^7.1.3, glob@^7.1.6:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||
@@ -1830,11 +1863,16 @@ ieee754@^1.1.13:
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ignore@^5.2.0, ignore@^5.3.1:
|
||||
ignore@^5.2.0:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
ignore@^7.0.0:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.4.tgz#a12c70d0f2607c5bf508fb65a40c75f037d7a078"
|
||||
integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@@ -1945,6 +1983,11 @@ is-plain-obj@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
|
||||
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
|
||||
|
||||
is-unicode-supported@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
@@ -2244,7 +2287,7 @@ minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@@ -2363,6 +2406,11 @@ next-electron-server@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-1.0.0.tgz#03e133ed64a5ef671b6c6409f908c4901b1828cb"
|
||||
integrity sha512-fTUaHwT0Jry2fbdUSIkAiIqgDAInI5BJFF4/j90/okvZCYlyx6yxpXB30KpzmOG6TN/ESwyvsFJVvS2WHT8PAA==
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
node-abi@^3.45.0:
|
||||
version "3.67.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.67.0.tgz#1d159907f18d18e18809dbbb5df47ed2426a08df"
|
||||
@@ -2399,6 +2447,13 @@ normalize-url@^6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
|
||||
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
|
||||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
@@ -2463,6 +2518,11 @@ p-cancelable@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
|
||||
integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
|
||||
|
||||
p-limit@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
|
||||
@@ -2535,6 +2595,11 @@ path-is-absolute@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
|
||||
|
||||
path-key@^2.0.0, path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
|
||||
|
||||
path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
@@ -2599,13 +2664,13 @@ prettier-plugin-organize-imports@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f"
|
||||
integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==
|
||||
|
||||
prettier-plugin-packagejson@^2.5.10:
|
||||
version "2.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7"
|
||||
integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==
|
||||
prettier-plugin-packagejson@^2.5.14:
|
||||
version "2.5.14"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.14.tgz#8ada09114ff60c7f42c3f8755ffb2f8152f3624f"
|
||||
integrity sha512-h+3tSpr2nVpp+YOK1MDIYtYhHVXr8/0V59UUbJpIJFaqi3w4fvUokJo6eV8W+vELrUXIZzJ+DKm5G7lYzrMcKQ==
|
||||
dependencies:
|
||||
sort-package-json "2.15.1"
|
||||
synckit "0.9.2"
|
||||
sort-package-json "3.2.1"
|
||||
synckit "0.11.6"
|
||||
|
||||
prettier@3.5.3:
|
||||
version "3.5.3"
|
||||
@@ -2829,6 +2894,11 @@ semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0, semve
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143"
|
||||
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
|
||||
|
||||
semver@^7.7.1:
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
serialize-error@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18"
|
||||
@@ -2836,6 +2906,13 @@ serialize-error@^7.0.1:
|
||||
dependencies:
|
||||
type-fest "^0.13.1"
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
|
||||
dependencies:
|
||||
shebang-regex "^1.0.0"
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
@@ -2843,6 +2920,11 @@ shebang-command@^2.0.0:
|
||||
dependencies:
|
||||
shebang-regex "^3.0.0"
|
||||
|
||||
shebang-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
|
||||
|
||||
shebang-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
@@ -2853,24 +2935,25 @@ shell-quote@^1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
|
||||
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
|
||||
|
||||
shelljs@^0.8.5:
|
||||
version "0.8.5"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
|
||||
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
|
||||
shelljs@^0.9.2:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.9.2.tgz#a8ac724434520cd7ae24d52071e37a18ac2bb183"
|
||||
integrity sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==
|
||||
dependencies:
|
||||
glob "^7.0.0"
|
||||
execa "^1.0.0"
|
||||
fast-glob "^3.3.2"
|
||||
interpret "^1.0.0"
|
||||
rechoir "^0.6.2"
|
||||
|
||||
shx@^0.3.4:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02"
|
||||
integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==
|
||||
shx@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/shx/-/shx-0.4.0.tgz#c6ea6ace7e778da0ab32d2eab9def59d788e9336"
|
||||
integrity sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==
|
||||
dependencies:
|
||||
minimist "^1.2.3"
|
||||
shelljs "^0.8.5"
|
||||
minimist "^1.2.8"
|
||||
shelljs "^0.9.2"
|
||||
|
||||
signal-exit@^3.0.2:
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
|
||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
|
||||
@@ -2923,19 +3006,18 @@ sort-object-keys@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
|
||||
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
|
||||
|
||||
sort-package-json@2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e"
|
||||
integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==
|
||||
sort-package-json@3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-3.2.1.tgz#889f3bdf43ceeff5fa4278a7c53ae5b1520d287e"
|
||||
integrity sha512-rTfRdb20vuoAn7LDlEtCqOkYfl2X+Qze6cLbNOzcDpbmKEhJI30tTN44d5shbKJnXsvz24QQhlCm81Bag7EOKg==
|
||||
dependencies:
|
||||
detect-indent "^7.0.1"
|
||||
detect-newline "^4.0.0"
|
||||
get-stdin "^9.0.0"
|
||||
git-hooks-list "^3.0.0"
|
||||
detect-newline "^4.0.1"
|
||||
git-hooks-list "^4.0.0"
|
||||
is-plain-obj "^4.1.0"
|
||||
semver "^7.6.0"
|
||||
semver "^7.7.1"
|
||||
sort-object-keys "^1.1.3"
|
||||
tinyglobby "^0.2.9"
|
||||
tinyglobby "^0.2.12"
|
||||
|
||||
source-map-support@^0.5.19:
|
||||
version "0.5.21"
|
||||
@@ -2990,6 +3072,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-eof@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
|
||||
|
||||
strip-json-comments@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
@@ -3021,13 +3108,12 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
synckit@0.9.2:
|
||||
version "0.9.2"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62"
|
||||
integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==
|
||||
synckit@0.11.6:
|
||||
version "0.11.6"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.6.tgz#e742a0c27bbc1fbc96f2010770521015cca7ed5c"
|
||||
integrity sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.1.0"
|
||||
tslib "^2.6.2"
|
||||
"@pkgr/core" "^0.2.4"
|
||||
|
||||
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
|
||||
version "6.2.1"
|
||||
@@ -3078,12 +3164,12 @@ tiny-typed-emitter@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"
|
||||
integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==
|
||||
|
||||
tinyglobby@^0.2.9:
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f"
|
||||
integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==
|
||||
tinyglobby@^0.2.12:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e"
|
||||
integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==
|
||||
dependencies:
|
||||
fdir "^6.4.2"
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
tmp-promise@^3.0.2:
|
||||
@@ -3117,12 +3203,12 @@ truncate-utf8-bytes@^1.0.0:
|
||||
dependencies:
|
||||
utf8-byte-length "^1.0.1"
|
||||
|
||||
ts-api-utils@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd"
|
||||
integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==
|
||||
ts-api-utils@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
|
||||
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
|
||||
|
||||
tslib@^2.1.0, tslib@^2.6.2:
|
||||
tslib@^2.1.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
@@ -3149,14 +3235,14 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
typescript-eslint@^8.31.1:
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b"
|
||||
integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==
|
||||
typescript-eslint@^8.32.1:
|
||||
version "8.32.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.32.1.tgz#1784335c781491be528ff84ab666e2f0f7591fd1"
|
||||
integrity sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.31.1"
|
||||
"@typescript-eslint/parser" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/eslint-plugin" "8.32.1"
|
||||
"@typescript-eslint/parser" "8.32.1"
|
||||
"@typescript-eslint/utils" "8.32.1"
|
||||
|
||||
typescript@^5.4.3, typescript@^5.8.3:
|
||||
version "5.8.3"
|
||||
@@ -3230,6 +3316,13 @@ wcwidth@^1.0.1:
|
||||
dependencies:
|
||||
defaults "^1.0.3"
|
||||
|
||||
which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
which@^2.0.1, which@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
@@ -3311,3 +3404,8 @@ yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
zod@^3.25.23:
|
||||
version "3.25.23"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.23.tgz#128fb02f3619a8bca6bbbf6b07b457236cf33391"
|
||||
integrity sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg==
|
||||
|
||||
@@ -339,6 +339,10 @@ export const sidebar = [
|
||||
text: "Backups",
|
||||
link: "/self-hosting/faq/backup",
|
||||
},
|
||||
{
|
||||
text: "Environment variables",
|
||||
link: "/self-hosting/faq/environment",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -21,7 +21,7 @@ always available on your machine.
|
||||
background without you needing to run any other cron jobs. See
|
||||
[migration/export](/photos/migration/export/) for more details.
|
||||
|
||||
## Does the exported data preserve folder structure?
|
||||
## Does the exported data preserve album structure?
|
||||
|
||||
Yes. When you export your data for local backup, it will maintain the exact
|
||||
album structure how you have set up within Ente.
|
||||
|
||||
@@ -26,9 +26,6 @@ unsupported file format and we will do our best to help you out.
|
||||
|
||||
Yes, we currently do not support files larger than 4 GB.
|
||||
|
||||
If this constraint is a concern for you, please write to
|
||||
[support@ente.io](mailto:support@ente.io) with your use case and we will do our
|
||||
best to help you.
|
||||
|
||||
## Does Ente support videos?
|
||||
|
||||
|
||||
@@ -8,22 +8,43 @@ description:
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Video streaming is available in beta on mobile apps starting v0.9.98.
|
||||
> Video streaming is available in beta on mobile apps starting v0.9.98 and on
|
||||
> desktop starting v1.7.13.
|
||||
|
||||
### How to enable video streaming?
|
||||
|
||||
#### On mobile
|
||||
|
||||
- Open Settings -> General -> Advanced
|
||||
- Switch on the toggle for `Video streaming`
|
||||
|
||||
#### On desktop
|
||||
|
||||
- Open Settings -> Preferences
|
||||
- Enable the toggle for `Streamable videos`
|
||||
|
||||
### What happens when I enable video streaming?
|
||||
|
||||
#### On mobile
|
||||
|
||||
Enabling video streaming will start processing videos captured in the last 30
|
||||
days, generating streams for each. Both local and remote videos will be
|
||||
processed, so this may consume bandwidth for downloading of remote files and
|
||||
uploading of the generated streams.
|
||||
|
||||
#### On desktop
|
||||
|
||||
When enabled, the desktop app will generate streams both for new uploads, and
|
||||
also for all existing videos that were previously uploaded.
|
||||
|
||||
Stream generation is CPU intensive and can take time so the app will continue
|
||||
processing them in the background. Clicking on search bar will show "Processing
|
||||
videos..." when stream generation is happening.
|
||||
|
||||
### How can I view video streams?
|
||||
|
||||
### On mobile
|
||||
|
||||
Settings -> Backup > Backup status will show details regarding the processing
|
||||
status for videos. Processed videos will have a green play button next to them.
|
||||
You can open these videos by tapping on them.
|
||||
@@ -34,6 +55,12 @@ play the stream.
|
||||
Clicking on the `Info` icon within the original video will show details about
|
||||
the generated stream.
|
||||
|
||||
### On desktop and web
|
||||
|
||||
Desktop and web app will automatically play the streaming version of a video if
|
||||
it has been already generated. The quality selector will show "Auto" when
|
||||
playing the stream.
|
||||
|
||||
### What is a stream?
|
||||
|
||||
Stream is an encrypted HLS file with an `.m3u8` playlist that helps play a video
|
||||
|
||||
@@ -14,9 +14,21 @@ directly stream chunks of Google Takeout zips that are stored on network drives.
|
||||
|
||||
In particular, the folder watch functionality suffers a lot since the app needs
|
||||
access to file system events to detect changes to the users files so that they
|
||||
can be uploaded whenever there are changes.
|
||||
can be uploaded whenever there are changes. Network drives are less reliable in
|
||||
providing these file change events correctly.
|
||||
|
||||
Since are high chances of the user having a subpar experience, we request
|
||||
customers to avoid using the desktop app directly with network attached storage
|
||||
and instead temporarily copy the files to their local storage for uploads, and
|
||||
avoid watching folders that live on a network drive.
|
||||
|
||||
## Exporting to UNC paths
|
||||
|
||||
Generally, exports are likely to work better than imports, since the interaction
|
||||
with the file system is relatively simpler (Note that the app still needs to
|
||||
scan the folder to find existing files, esp. if the continuous export option is
|
||||
enabled).
|
||||
|
||||
A special case is when exporting to a UNC path. In this case, the file
|
||||
separators will not work as expected and the export will not start. As a
|
||||
workaround, you can map your UNC path to a network drive and use that instead.
|
||||
|
||||
49
docs/docs/self-hosting/faq/environment.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: "Environment Variables and Ports"
|
||||
description: "Information about all the Environment Variables needed to run Ente"
|
||||
---
|
||||
|
||||
# Environment variables and ports
|
||||
A self-hosted Ente instance requires specific endpoints in both Museum (the server) and web apps. This document outlines the essential environment variables and port mappings of the web apps.
|
||||
|
||||
Here's the list of important variables that a self hoster should know about:
|
||||
|
||||
### Museum
|
||||
|
||||
1. `NEXT_PUBLIC_ENTE_ENDPOINT`
|
||||
|
||||
The above environment variable is used to configure Museums endpoint. Where Museum is
|
||||
running and which port it is listening on. This endpoint should be configured for
|
||||
all the apps to connect to your self hosted endpoint.
|
||||
|
||||
All the apps (regardless of platform) by default connect to api.ente.io - which is
|
||||
our production instance of Museum.
|
||||
|
||||
### Web Apps
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Web apps don't need to be configured with the below endpoints. Web app environment
|
||||
> variables are being documented here just so that the users know everything in detail.
|
||||
> Checkout [Configuring your Server](/self-hosting/museum) to configure endpoints for
|
||||
> particular app.
|
||||
|
||||
In Ente, all the web apps are separate NextJS applications. Therefore, they are all
|
||||
configured via environment variables. The photos app (Ente Photos) has information
|
||||
about and connects to other web apps like albums, cast, etc.
|
||||
|
||||
|
||||
1. `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT`
|
||||
|
||||
This environment variable is used to configure and declare the endpoint for the Albums
|
||||
web app.
|
||||
|
||||
## Ports
|
||||
|
||||
The below format is according to how ports are mapped in Docker.
|
||||
Typically,`<host>:<container-port>`
|
||||
|
||||
1. `8080:8080`: Museum (Ente's server)
|
||||
2. `3000:3000`: Ente Photos web app
|
||||
3. `3001:3001`: Ente Accounts web app
|
||||
4. `3003:3003`: [Ente Auth web app](https://ente.io/auth/)
|
||||
5. `3004:3004`: [Ente Cast web app](http://ente.io/cast)
|
||||
@@ -3,32 +3,6 @@ title: Server admin
|
||||
description: Administering your custom self-hosted Ente instance using the CLI
|
||||
---
|
||||
|
||||
# Administering your custom server
|
||||
|
||||
You can use
|
||||
[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to
|
||||
administer your self hosted server.
|
||||
|
||||
First we need to get your CLI to connect to your custom server. Define a
|
||||
config.yaml and put it either in the same directory as CLI or path defined in
|
||||
env variable `ENTE_CLI_CONFIG_PATH`
|
||||
|
||||
```yaml
|
||||
endpoint:
|
||||
api: "http://localhost:8080"
|
||||
```
|
||||
|
||||
Now you should be able to
|
||||
[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md),
|
||||
and subsequently increase the
|
||||
[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md)
|
||||
using the CLI.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The CLI command to add an account does not create Ente accounts. It only adds
|
||||
> existing accounts to the list of (existing) accounts that the CLI can use.
|
||||
|
||||
## Becoming an admin
|
||||
|
||||
By default, the first user (and only the first user) created on the system is
|
||||
@@ -40,7 +14,7 @@ explicit whitelist of admins.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The first user is only treated as the admin if the list of admins in the
|
||||
> The first user is only treated as the admin if the list of admins in the
|
||||
> configuration is empty.
|
||||
>
|
||||
> Also, if at some point you delete the first user, then you will need to define
|
||||
@@ -78,6 +52,38 @@ You can use
|
||||
[account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md)
|
||||
command to find the user id of any account.
|
||||
|
||||
# Administering your custom server
|
||||
|
||||
> [!NOTE]
|
||||
> For the first user (admin) to perform administrative actions using the CLI, their
|
||||
> userID must be whitelisted in the `museum.yaml` configuration file under
|
||||
> `internal.admins`. While the first user is automatically granted admin privileges
|
||||
> on the server, this additional step is required for CLI operations.
|
||||
|
||||
You can use
|
||||
[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to
|
||||
administer your self hosted server.
|
||||
|
||||
First we need to get your CLI to connect to your custom server. Define a
|
||||
config.yaml and put it either in the same directory as CLI or path defined in
|
||||
env variable `ENTE_CLI_CONFIG_PATH`
|
||||
|
||||
```yaml
|
||||
endpoint:
|
||||
api: "http://localhost:8080"
|
||||
```
|
||||
|
||||
Now you should be able to
|
||||
[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md),
|
||||
and subsequently increase the
|
||||
[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md)
|
||||
using the CLI.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The CLI command to add an account does not create Ente accounts. It only adds
|
||||
> existing accounts to the list of (existing) accounts that the CLI can use.
|
||||
|
||||
## Backups
|
||||
|
||||
See this [FAQ](/self-hosting/faq/backup).
|
||||
|
||||
@@ -49,13 +49,17 @@ For example,
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
public-albums: albums.myente.xyz
|
||||
cast: cast.myente.xyz
|
||||
accounts: accounts.myente.xyz
|
||||
family: family.myente.xyz
|
||||
public-albums: https://albums.myente.xyz
|
||||
cast: https://cast.myente.xyz
|
||||
accounts: https://accounts.myente.xyz
|
||||
family: https://family.myente.xyz
|
||||
```
|
||||
|
||||
By default, all the values redirect to our publicly hosted production services.
|
||||
>[!IMPORTANT]
|
||||
>By default, all the values redirect to our publicly hosted production services.
|
||||
>For example, if `public-albums` is not configured your shared album will
|
||||
>use the `albums.ente.io` URL.
|
||||
|
||||
After you are done with filling the values, restart museum and the app will
|
||||
start utilizing those endpoints instead of Ente's production instances.
|
||||
|
||||
@@ -67,3 +71,7 @@ Similarly, you can use the default
|
||||
[`local.yaml`](https://github.com/ente-io/ente/tree/main/server/configurations/local.yaml)
|
||||
as a reference for building a functioning `museum.yaml` for many other
|
||||
functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc.
|
||||
|
||||
## References
|
||||
|
||||
- [Environment variables and ports](/self-hosting/faq/environment)
|
||||
|
||||
@@ -3,14 +3,14 @@ title: Uploads
|
||||
description: Fixing upload errors when trying to self host Ente
|
||||
---
|
||||
|
||||
# Troubleshooting upload failures
|
||||
# Troubleshooting upload failures
|
||||
|
||||
Here are some errors our community members frequently encountered with the
|
||||
context and potential fixes.
|
||||
|
||||
Fundamentally in most situations, the problem is because of minor mistakes or
|
||||
misconfiguration. Please make sure to reverse proxy museum and MinIO API
|
||||
endpoint to a domain and check your S3 credentials and whole configuration
|
||||
misconfiguration. Please make sure to reverse proxy museum and MinIO API
|
||||
endpoint to a domain and check your S3 credentials and whole configuration
|
||||
file for any minor misconfigurations.
|
||||
|
||||
It is also suggested that the user setups bucket CORS on MinIO or any external
|
||||
@@ -21,10 +21,10 @@ this](/self-hosting/troubleshooting/bucket-cors).
|
||||
|
||||
S3 is an cloud storage protocol made by Amazon (specifically AWS). S3 is designed to store
|
||||
files and data as objects inside Buckets and it is mostly used for Online
|
||||
Backups and storing different types of files.
|
||||
Backups and storing different types of files.
|
||||
|
||||
Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 provider.
|
||||
MinIO supports the Amazon S3 protocol and leverages your disk storage to
|
||||
Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 provider.
|
||||
MinIO supports the Amazon S3 protocol and leverages your disk storage to
|
||||
dump all the uploaded files as encrypted object blobs.
|
||||
|
||||
## 403 Forbidden
|
||||
@@ -40,15 +40,15 @@ This could be because
|
||||
|
||||
1. The bucket CORS rules do not allow museum to access these objects. For
|
||||
uploading files from the browser, you will need to set `allowedOrigins` to
|
||||
`*`, and allow the `X-Auth-Token`, `X-Client-Package` headers configuration
|
||||
too. [Here is an example of a working
|
||||
`*`, and allow the `X-Auth-Token`, `X-Client-Package`, `X-Client-Version`
|
||||
headers configuration too. [Here is an example of a working
|
||||
configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
|
||||
|
||||
2. The credentials are not being picked up (you might be setting the correct
|
||||
credentials, but not in the place where museum reads them from).
|
||||
|
||||
## Mismatch in file size
|
||||
## Mismatch in file size
|
||||
|
||||
The "Mismatch in file size" error mostly occurs in a situation where the client is re-uploading a file which is already in the bucket with a different
|
||||
file size. The reason for re-upload could be anything including network issue,
|
||||
sudden killing of app before the upload is complete and etc.
|
||||
sudden killing of app before the upload is complete and etc.
|
||||
|
||||
@@ -36,7 +36,7 @@ else
|
||||
BACKUP_ID=$(scw rdb backup create instance-id=$SCW_RDB_INSTANCE_ID \
|
||||
name=$BACKUP_NAME expires-at=$EXPIRY \
|
||||
database-name=ente_db -o json | jq -r '.id')
|
||||
scw rdb backup wait $BACKUP_ID timeout=5h
|
||||
scw rdb backup wait $BACKUP_ID timeout=8h
|
||||
scw rdb backup download output=$BACKUP_FILE \
|
||||
$(scw rdb backup export $BACKUP_ID --wait -o json | jq -r '.id')
|
||||
fi
|
||||
|
||||
@@ -10,10 +10,10 @@ import "./App.css";
|
||||
import FamilyTableComponent from "./components/FamilyComponentTable";
|
||||
import StorageBonusTableComponent from "./components/StorageBonusTableComponent";
|
||||
import TokensTableComponent from "./components/TokenTableComponent";
|
||||
import type { UserData } from "./components/UserComponent";
|
||||
import UserComponent from "./components/UserComponent";
|
||||
import duckieimage from "./components/duckie.png";
|
||||
import { apiOrigin } from "./services/support";
|
||||
import type { UserData, UserResponse } from "./types";
|
||||
|
||||
export let email = "";
|
||||
export let token = "";
|
||||
@@ -29,38 +29,6 @@ export const setToken = (newToken: string) => {
|
||||
export const getEmail = () => email;
|
||||
export const getToken = () => token;
|
||||
|
||||
interface User {
|
||||
ID: string;
|
||||
email: string;
|
||||
creationTime: number;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
productID: string;
|
||||
paymentProvider: string;
|
||||
expiryTime: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
interface Security {
|
||||
isEmailMFAEnabled: boolean;
|
||||
isTwoFactorEnabled: boolean;
|
||||
passkeys: string;
|
||||
passkeyCount: number;
|
||||
canDisableEmailMFA: boolean;
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
user: User;
|
||||
subscription: Subscription;
|
||||
authCodes?: number;
|
||||
details?: {
|
||||
usage?: number;
|
||||
storageBonus?: number;
|
||||
profileData: Security;
|
||||
};
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [localEmail, setLocalEmail] = useState<string>("");
|
||||
const [localToken, setLocalToken] = useState<string>("");
|
||||
@@ -139,7 +107,7 @@ const App: React.FC = () => {
|
||||
console.log("API Response:", userDataResponse);
|
||||
|
||||
const extractedUserData: UserData = {
|
||||
User: {
|
||||
user: {
|
||||
"User ID": userDataResponse.user.ID || "None",
|
||||
Email: userDataResponse.user.email || "None",
|
||||
"Creation time":
|
||||
@@ -147,7 +115,7 @@ const App: React.FC = () => {
|
||||
userDataResponse.user.creationTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Storage: {
|
||||
storage: {
|
||||
Total: userDataResponse.subscription.storage
|
||||
? userDataResponse.subscription.storage >= 1024 ** 3
|
||||
? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB`
|
||||
@@ -166,7 +134,7 @@ const App: React.FC = () => {
|
||||
: `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB`
|
||||
: "None",
|
||||
},
|
||||
Subscription: {
|
||||
subscription: {
|
||||
"Product ID":
|
||||
userDataResponse.subscription.productID || "None",
|
||||
Provider:
|
||||
@@ -176,7 +144,7 @@ const App: React.FC = () => {
|
||||
userDataResponse.subscription.expiryTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Security: {
|
||||
security: {
|
||||
"Email MFA": userDataResponse.details?.profileData
|
||||
.isEmailMFAEnabled
|
||||
? "Enabled"
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getEmail, getToken } from "../App";
|
||||
import { apiOrigin } from "../services/support";
|
||||
interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
import type { ErrorResponse } from "../types";
|
||||
|
||||
// The below interfaces will only be used in this file
|
||||
// hence not including them into a sub-merged types file
|
||||
interface ChangeEmailProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface CloseFamilyProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface Disable2FAProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -10,20 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
|
||||
interface DisablePasskeysProps {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
|
||||
}
|
||||
import type { DisablePasskeysProps, UserData } from "../types";
|
||||
|
||||
const DisablePasskeys: React.FC<DisablePasskeysProps> = ({
|
||||
open,
|
||||
|
||||
@@ -13,23 +13,10 @@ import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getEmail, getToken } from "../App";
|
||||
import { apiOrigin } from "../services/support";
|
||||
import type { FamilyMember, UserData } from "../types";
|
||||
import { formatUsageToGB } from "../utils/";
|
||||
import CloseFamily from "./CloseFamily";
|
||||
|
||||
interface FamilyMember {
|
||||
id: string;
|
||||
email: string;
|
||||
status: string;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
details: {
|
||||
familyData: {
|
||||
members: FamilyMember[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const FamilyTableComponent: React.FC = () => {
|
||||
const [familyMembers, setFamilyMembers] = useState<FamilyMember[]>([]);
|
||||
const [closeFamilyOpen, setCloseFamilyOpen] = useState(false);
|
||||
@@ -54,7 +41,7 @@ const FamilyTableComponent: React.FC = () => {
|
||||
}
|
||||
const userData = (await response.json()) as UserData; // Typecast to UserData interface
|
||||
const members: FamilyMember[] =
|
||||
userData.details.familyData.members;
|
||||
userData.details?.familyData.members ?? [];
|
||||
setFamilyMembers(members);
|
||||
} catch (error) {
|
||||
console.error("Error fetching family data:", error);
|
||||
@@ -69,11 +56,6 @@ const FamilyTableComponent: React.FC = () => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const formatUsageToGB = (usage: number): string => {
|
||||
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
|
||||
return `${usageInGB} GB`;
|
||||
};
|
||||
|
||||
const handleOpenCloseFamily = () => {
|
||||
setCloseFamilyOpen(true);
|
||||
};
|
||||
@@ -111,6 +93,9 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<Table aria-label="family-table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<b>ID</b>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<b>User</b>
|
||||
</TableCell>
|
||||
@@ -121,13 +106,14 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<b>Usage</b>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<b>ID</b>
|
||||
<b>Quota</b>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{familyMembers.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>{member.id}</TableCell>
|
||||
<TableCell>{member.email}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
@@ -152,7 +138,15 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<TableCell>
|
||||
{formatUsageToGB(member.usage)}
|
||||
</TableCell>
|
||||
<TableCell>{member.id}</TableCell>
|
||||
<TableCell>
|
||||
{member.status !== "SELF"
|
||||
? (member.storageLimit &&
|
||||
formatUsageToGB(
|
||||
member.storageLimit,
|
||||
)) ||
|
||||
"NA"
|
||||
: ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface ToggleEmailMFAProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -62,8 +62,8 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
expiryTime: "",
|
||||
userId: "",
|
||||
attributes: {
|
||||
"customerID": "",
|
||||
"stripeAccountCountry": ""
|
||||
customerID: "",
|
||||
stripeAccountCountry: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,9 +108,13 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
expiryTime: expiryTime,
|
||||
userId: userDataResponse.subscription.userID || "",
|
||||
attributes: {
|
||||
customerID: userDataResponse.subscription.attributes.customerID || "",
|
||||
stripeAccountCountry: userDataResponse.subscription.attributes.stripeAccountCountry || ""
|
||||
}
|
||||
customerID:
|
||||
userDataResponse.subscription.attributes
|
||||
.customerID || "",
|
||||
stripeAccountCountry:
|
||||
userDataResponse.subscription.attributes
|
||||
.stripeAccountCountry || "",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
@@ -174,8 +178,9 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
transactionId: values.transactionId,
|
||||
attributes: {
|
||||
customerID: values.attributes.customerID,
|
||||
stripeAccountCountry: values.attributes.stripeAccountCountry
|
||||
}
|
||||
stripeAccountCountry:
|
||||
values.attributes.stripeAccountCountry,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import type { UserComponentProps } from "../types";
|
||||
import ChangeEmail from "./ChangeEmail";
|
||||
import DeleteAccount from "./DeleteAccont";
|
||||
import Disable2FA from "./Disable2FA";
|
||||
@@ -20,17 +21,6 @@ import DisablePasskeys from "./DisablePasskeys";
|
||||
import ToggleEmailMFA from "./ToggleEmailMFA";
|
||||
import UpdateSubscription from "./UpdateSubscription";
|
||||
|
||||
export interface UserData {
|
||||
User: Record<string, string>;
|
||||
Storage: Record<string, string>;
|
||||
Subscription: Record<string, string>;
|
||||
Security: Record<string, string>;
|
||||
}
|
||||
|
||||
interface UserComponentProps {
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
|
||||
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
|
||||
const [email2FAEnabled, setEmail2FAEnabled] = React.useState(false);
|
||||
@@ -44,10 +34,10 @@ const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
|
||||
const [disablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTwoFactorEnabled(userData?.Security["Two factor 2FA"] === "Enabled");
|
||||
setEmail2FAEnabled(userData?.Security["Email MFA"] === "Enabled");
|
||||
setTwoFactorEnabled(userData?.security["Two factor 2FA"] === "Enabled");
|
||||
setEmail2FAEnabled(userData?.security["Email MFA"] === "Enabled");
|
||||
setCanDisableEmailMFA(
|
||||
userData?.Security["Can Disable EmailMFA"] === "Yes",
|
||||
userData?.security["Can Disable EmailMFA"] === "Yes",
|
||||
);
|
||||
}, [userData]);
|
||||
|
||||
@@ -148,14 +138,10 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
minHeight: 300,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginBottom: "20px",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
padding: "13px",
|
||||
padding: "10px",
|
||||
overflowX: "hidden",
|
||||
"&:not(:last-child)": {
|
||||
marginBottom: "40px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -176,9 +162,9 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
{title.charAt(0).toUpperCase() + title.slice(1)}
|
||||
</Typography>
|
||||
{title === "User" && (
|
||||
{title === "user" && (
|
||||
<IconButton
|
||||
edge="start"
|
||||
aria-label="delete"
|
||||
@@ -187,7 +173,7 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
<DeleteIcon style={{ color: "" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{title === "Subscription" && (
|
||||
{title === "subscription" && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
|
||||
71
infra/staff/src/types/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Type related Users
|
||||
|
||||
export interface User {
|
||||
ID: string;
|
||||
email: string;
|
||||
creationTime: number;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
user: User;
|
||||
subscription: Subscription;
|
||||
authCodes?: number;
|
||||
details?: {
|
||||
usage?: number;
|
||||
storageBonus?: number;
|
||||
profileData: Security;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
user: Record<string, string>;
|
||||
storage: Record<string, string>;
|
||||
subscription?: Record<string, string>;
|
||||
security: Record<string, string>;
|
||||
details?: {
|
||||
familyData: {
|
||||
members: FamilyMember[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserComponentProps {
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
// Error Response Interface
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Types related to Subscriptions
|
||||
export interface Subscription {
|
||||
productID: string;
|
||||
paymentProvider: string;
|
||||
expiryTime: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
export interface Security {
|
||||
isEmailMFAEnabled: boolean;
|
||||
isTwoFactorEnabled: boolean;
|
||||
passkeys: string;
|
||||
passkeyCount: number;
|
||||
canDisableEmailMFA: boolean;
|
||||
}
|
||||
|
||||
// Types related Family
|
||||
export interface FamilyMember {
|
||||
id: string;
|
||||
email: string;
|
||||
status: string;
|
||||
usage: number;
|
||||
storageLimit: number;
|
||||
}
|
||||
|
||||
// Types related to passkeys
|
||||
export interface DisablePasskeysProps {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
|
||||
}
|
||||
5
infra/staff/src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Common utilities
|
||||
export function formatUsageToGB(usage: number): string {
|
||||
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
|
||||
return `${usageInGB} GB`;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* User facing strings in the app.
|
||||
*
|
||||
* By keeping them separate, we make our lives easier if/when we need to
|
||||
* localize the corresponding pages. Right now, these are just the values in the
|
||||
* default language, English.
|
||||
*/
|
||||
const S = {
|
||||
hello: "Hello Ente!",
|
||||
error_generic: "Oops, something went wrong.",
|
||||
};
|
||||
|
||||
export default S;
|
||||
5
infra/workers/csp-reporter/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "csp-reporter",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
23
infra/workers/csp-reporter/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Log CSP reports.
|
||||
*
|
||||
* See _headers in the web app source.
|
||||
*/
|
||||
|
||||
export default {
|
||||
async fetch(request: Request) {
|
||||
switch (request.method) {
|
||||
case "POST":
|
||||
return handlePOST(request);
|
||||
default:
|
||||
console.log(`Unsupported HTTP method ${request.method}`);
|
||||
return new Response(null, { status: 405 });
|
||||
}
|
||||
},
|
||||
} satisfies ExportedHandler;
|
||||
|
||||
const handlePOST = async (request: Request) => {
|
||||
// {job="worker"} |= `[csp-report]` | json log="logs[0]" | keep log
|
||||
console.log("[csp-report]", await request.text());
|
||||
return new Response(null, { status: 200 });
|
||||
};
|
||||
1
infra/workers/csp-reporter/tsconfig.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "extends": "../tsconfig.base.json", "include": ["src"] }
|
||||
9
infra/workers/csp-reporter/wrangler.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
name = "csp-reporter"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-05-19"
|
||||
|
||||
routes = [
|
||||
{ pattern = "csp-reporter.ente.io", custom_domain = true }
|
||||
]
|
||||
|
||||
tail_consumers = [{ service = "tail" }]
|
||||
@@ -21,7 +21,7 @@ const handleOPTIONS = (request: Request) => {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package",
|
||||
"Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package, X-Client-Version",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "workers",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"typescript": "^5",
|
||||
"wrangler": "^3",
|
||||
"prettier": "^3.3.4"
|
||||
"@cloudflare/workers-types": "^4.20250519.0",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.15.2",
|
||||
"prettier": "^3.5.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"*"
|
||||
|
||||
@@ -22,7 +22,7 @@ const handleOPTIONS = (request: Request) => {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package",
|
||||
"X-Auth-Access-Token, X-Auth-Access-Token-JWT, X-Client-Package, X-Client-Version",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ const handleOPTIONS = (request: Request) => {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package",
|
||||
"Access-Control-Allow-Headers": "X-Auth-Token, X-Client-Package, X-Client-Version",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ const handleOPTIONS = (request: Request) => {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, PUT, OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"Content-Type, UPLOAD-URL, X-Client-Package",
|
||||
"Content-Type, UPLOAD-URL, X-Client-Package, X-Client-Version",
|
||||
"Access-Control-Expose-Headers": "X-Request-Id, CF-Ray",
|
||||
"Access-Control-Max-Age": "86400",
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ analyzer:
|
||||
sort_child_properties_last: warning
|
||||
sort_pub_dependencies: warning
|
||||
library_private_types_in_public_api: warning
|
||||
constant_identifier_names: warning
|
||||
constant_identifier_names: ignore
|
||||
prefer_const_constructors: warning
|
||||
prefer_const_declarations: warning
|
||||
prefer_const_constructors_in_immutables: warning
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:largeHeap="true">
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTask"
|
||||
android:theme="@style/LaunchTheme"
|
||||
@@ -150,6 +153,29 @@
|
||||
<meta-data android:name="android.appwidget.provider"
|
||||
android:resource="@xml/memory_widget" />
|
||||
</receiver>
|
||||
<receiver android:name="EnteAlbumsWidgetProvider" android:label="Albums" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider"
|
||||
android:resource="@xml/albums_widget" />
|
||||
</receiver>
|
||||
<receiver android:name="EntePeopleWidgetProvider" android:label="People" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider"
|
||||
android:resource="@xml/people_widget" />
|
||||
</receiver>
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" /> <!-- Needed for scheduling notifications -->
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"> <!-- Needed for scheduling notifications -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
<!-- Android 11: https://developer.android.com/preview/privacy/package-visibility -->
|
||||
@@ -177,4 +203,6 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="com.android.vending.BILLING" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- Needed for scheduling notifications -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- Needed for scheduling notifications -->
|
||||
</manifest>
|
||||
@@ -0,0 +1,195 @@
|
||||
package io.ente.photos
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.ContextCompat
|
||||
import es.antonborri.home_widget.HomeWidgetLaunchIntent
|
||||
import es.antonborri.home_widget.HomeWidgetProvider
|
||||
import java.io.File
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class AlbumsFileData(
|
||||
val title: String?,
|
||||
val subText: String?,
|
||||
val generatedId: Int?,
|
||||
val mainKey: String?
|
||||
)
|
||||
|
||||
class EnteAlbumsWidgetProvider : HomeWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
widgetData: SharedPreferences
|
||||
) {
|
||||
appWidgetIds.forEach { widgetId ->
|
||||
val views =
|
||||
RemoteViews(context.packageName, R.layout.albums_widget_layout)
|
||||
.apply {
|
||||
val totalAlbums =
|
||||
widgetData.getInt("totalAlbums", 0)
|
||||
var randomNumber = -1
|
||||
var imagePath: String? = null
|
||||
if (totalAlbums > 0) {
|
||||
randomNumber =
|
||||
(0 until totalAlbums!!).random()
|
||||
imagePath =
|
||||
widgetData.getString(
|
||||
"albums_widget_" +
|
||||
randomNumber,
|
||||
null
|
||||
)
|
||||
}
|
||||
var imageExists: Boolean = false
|
||||
if (imagePath != null) {
|
||||
val imageFile = File(imagePath)
|
||||
imageExists = imageFile.exists()
|
||||
}
|
||||
if (imageExists) {
|
||||
val data =
|
||||
widgetData.getString(
|
||||
"albums_widget_${randomNumber}_data",
|
||||
null
|
||||
)
|
||||
val decoded: AlbumsFileData? =
|
||||
data?.let {
|
||||
Json.decodeFromString<
|
||||
AlbumsFileData>(it)
|
||||
}
|
||||
val title = decoded?.title
|
||||
val subText = decoded?.subText
|
||||
val generatedId = decoded?.generatedId
|
||||
val mainKey = decoded?.mainKey
|
||||
|
||||
val deepLinkUri =
|
||||
Uri.parse(
|
||||
"albumwidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget"
|
||||
)
|
||||
|
||||
val pendingIntent =
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
deepLinkUri
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_container,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"EnteAlbumsWidgetProvider",
|
||||
"Image exists: $imagePath"
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_img,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_title,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_overlay,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_text,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.GONE
|
||||
)
|
||||
|
||||
val bitmap: Bitmap =
|
||||
BitmapFactory.decodeFile(imagePath)
|
||||
setImageViewBitmap(R.id.widget_img, bitmap)
|
||||
setTextViewText(R.id.widget_title, title)
|
||||
setTextViewText(
|
||||
R.id.widget_subtitle,
|
||||
subText
|
||||
)
|
||||
} else {
|
||||
// Open App on Widget Click
|
||||
val pendingIntent =
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java
|
||||
)
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_container,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"EnteAlbumsWidgetProvider",
|
||||
"Image doesn't exists"
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_img,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_title,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_overlay,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_text,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.VISIBLE
|
||||
)
|
||||
|
||||
val drawable =
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_albums_widget
|
||||
)
|
||||
val bitmap =
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
setImageViewBitmap(
|
||||
R.id.widget_placeholder,
|
||||
bitmap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() {
|
||||
R.id.widget_img,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.VISIBLE
|
||||
@@ -148,10 +144,6 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() {
|
||||
R.id.widget_img,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.GONE
|
||||
@@ -181,7 +173,7 @@ class EnteMemoryWidgetProvider : HomeWidgetProvider() {
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable
|
||||
.ic_home_widget_default
|
||||
.ic_memories_widget
|
||||
)
|
||||
val bitmap =
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package io.ente.photos
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.ContextCompat
|
||||
import es.antonborri.home_widget.HomeWidgetLaunchIntent
|
||||
import es.antonborri.home_widget.HomeWidgetProvider
|
||||
import java.io.File
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class PeopleFileData(
|
||||
val title: String?,
|
||||
val subText: String?,
|
||||
val generatedId: Int?,
|
||||
val mainKey: String?
|
||||
)
|
||||
|
||||
class EntePeopleWidgetProvider : HomeWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
widgetData: SharedPreferences
|
||||
) {
|
||||
appWidgetIds.forEach { widgetId ->
|
||||
val views =
|
||||
RemoteViews(context.packageName, R.layout.people_widget_layout)
|
||||
.apply {
|
||||
val totalPeople =
|
||||
widgetData.getInt("totalPeople", 0)
|
||||
var randomNumber = -1
|
||||
var imagePath: String? = null
|
||||
if (totalPeople > 0) {
|
||||
randomNumber =
|
||||
(0 until totalPeople!!).random()
|
||||
imagePath =
|
||||
widgetData.getString(
|
||||
"people_widget_" +
|
||||
randomNumber,
|
||||
null
|
||||
)
|
||||
}
|
||||
var imageExists: Boolean = false
|
||||
if (imagePath != null) {
|
||||
val imageFile = File(imagePath)
|
||||
imageExists = imageFile.exists()
|
||||
}
|
||||
if (imageExists) {
|
||||
val data =
|
||||
widgetData.getString(
|
||||
"people_widget_${randomNumber}_data",
|
||||
null
|
||||
)
|
||||
val decoded: PeopleFileData? =
|
||||
data?.let {
|
||||
Json.decodeFromString<
|
||||
PeopleFileData>(it)
|
||||
}
|
||||
val title = decoded?.title
|
||||
val subText = decoded?.subText
|
||||
val generatedId = decoded?.generatedId
|
||||
val mainKey = decoded?.mainKey
|
||||
|
||||
val deepLinkUri =
|
||||
Uri.parse(
|
||||
"peoplewidget://message?generatedId=${generatedId}&mainKey=${mainKey}&homeWidget"
|
||||
)
|
||||
|
||||
val pendingIntent =
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
deepLinkUri
|
||||
)
|
||||
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_container,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"EntePeopleWidgetProvider",
|
||||
"Image exists: $imagePath"
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_img,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_title,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_overlay,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_text,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.GONE
|
||||
)
|
||||
|
||||
val bitmap: Bitmap =
|
||||
BitmapFactory.decodeFile(imagePath)
|
||||
setImageViewBitmap(R.id.widget_img, bitmap)
|
||||
setTextViewText(R.id.widget_title, title)
|
||||
setTextViewText(
|
||||
R.id.widget_subtitle,
|
||||
subText
|
||||
)
|
||||
} else {
|
||||
// Open App on Widget Click
|
||||
val pendingIntent =
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java
|
||||
)
|
||||
setOnClickPendingIntent(
|
||||
R.id.widget_container,
|
||||
pendingIntent
|
||||
)
|
||||
|
||||
Log.d(
|
||||
"EntePeopleWidgetProvider",
|
||||
"Image doesn't exists"
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_img,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_subtitle,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_title,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_overlay,
|
||||
View.GONE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_text,
|
||||
View.VISIBLE
|
||||
)
|
||||
setViewVisibility(
|
||||
R.id.widget_placeholder_container,
|
||||
View.VISIBLE
|
||||
)
|
||||
|
||||
val drawable =
|
||||
ContextCompat.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_people_widget
|
||||
)
|
||||
val bitmap =
|
||||
(drawable as BitmapDrawable).bitmap
|
||||
setImageViewBitmap(
|
||||
R.id.widget_placeholder,
|
||||
bitmap
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |