Compare commits
441 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 | ||
|
|
43cdd10e85 | ||
|
|
539145d38d | ||
|
|
6e4a856ea4 | ||
|
|
40ff361af1 | ||
|
|
63b0cee589 | ||
|
|
574eea58fc | ||
|
|
138310b8f8 | ||
|
|
3d63ded84d | ||
|
|
2ff69f661e | ||
|
|
10c65f13c8 | ||
|
|
761c976d7e | ||
|
|
423a7eec37 | ||
|
|
c8b23f80e2 | ||
|
|
d127199ade | ||
|
|
f44e90801f | ||
|
|
ded497d421 | ||
|
|
22bae0292d | ||
|
|
4c7121fd6c | ||
|
|
f53745bbb0 | ||
|
|
1c6e343994 | ||
|
|
2144f57ee0 | ||
|
|
476fe1b624 | ||
|
|
ff705280c7 | ||
|
|
6ff6594a81 | ||
|
|
69f12125ec | ||
|
|
862b11c530 | ||
|
|
e40afac212 | ||
|
|
a880726f16 | ||
|
|
bffd4d83a5 | ||
|
|
59a534225c | ||
|
|
810ee3d9fe | ||
|
|
9605637e50 | ||
|
|
9016394ccc | ||
|
|
a518bbd608 | ||
|
|
1c4ebcccb1 | ||
|
|
793fd5ba39 | ||
|
|
9b1eacf736 | ||
|
|
cae9988c9a | ||
|
|
538a5df32d | ||
|
|
853b916cf1 | ||
|
|
93d6f58660 | ||
|
|
7b145f0898 | ||
|
|
add09a601d | ||
|
|
5dda596544 | ||
|
|
8101bee2fd | ||
|
|
c234bc7be8 | ||
|
|
27fd372d62 | ||
|
|
66f23283c1 | ||
|
|
8d7bc81c20 | ||
|
|
41e870f7a0 | ||
|
|
0342e1ef56 | ||
|
|
efed42ce4a | ||
|
|
ef9d925686 | ||
|
|
e84f46f435 | ||
|
|
c1193be61c | ||
|
|
9b460ca1dc | ||
|
|
a101dba6cd | ||
|
|
491de296ca | ||
|
|
a614636789 | ||
|
|
07c640cf90 | ||
|
|
9fa13508b8 | ||
|
|
1c099a60e8 | ||
|
|
9170c80b26 | ||
|
|
6ae8abb492 | ||
|
|
f077213c62 | ||
|
|
62c1f0c6ac | ||
|
|
cc486983ab | ||
|
|
f22ad9611f | ||
|
|
9cafa72ae3 | ||
|
|
0c9d7321eb | ||
|
|
5fc5d0ef48 | ||
|
|
84017c7397 | ||
|
|
e250759999 | ||
|
|
f088c24abe | ||
|
|
e197423c1d | ||
|
|
1785baf4af | ||
|
|
51d55ee92b | ||
|
|
7cd3f8e2ac | ||
|
|
91fefa7eb9 | ||
|
|
63b9a09a2d | ||
|
|
6627f77d92 | ||
|
|
14b70ce66e | ||
|
|
d9a5fbfe00 | ||
|
|
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 |
@@ -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,6 +43,12 @@
|
||||
"title": "Anycoin Direct",
|
||||
"slug": "anycoindirect"
|
||||
},
|
||||
{
|
||||
"title": "AR24",
|
||||
"altNames": [
|
||||
"Docaposte AR24"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Aruba",
|
||||
"slug": "aruba",
|
||||
@@ -441,6 +447,9 @@
|
||||
"title": "Finanzfluss",
|
||||
"slug": "finanzfluss"
|
||||
},
|
||||
{
|
||||
"title": "Finary"
|
||||
},
|
||||
{
|
||||
"title": "Firefox",
|
||||
"slug": "mozilla"
|
||||
@@ -743,6 +752,7 @@
|
||||
{
|
||||
"title": "Mistral",
|
||||
"altNames": [
|
||||
"Le Chat",
|
||||
"Mistral AI",
|
||||
"MistralAI"
|
||||
]
|
||||
@@ -892,6 +902,10 @@
|
||||
"slug": "onshape",
|
||||
"hex": "7abb5e"
|
||||
},
|
||||
{
|
||||
"title": "Oracle Cloud",
|
||||
"slug": "oracle_cloud"
|
||||
},
|
||||
{
|
||||
"title": "Parqet",
|
||||
"slug": "parqet"
|
||||
|
||||
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 |
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": "الأرقام"
|
||||
}
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "Ungültiger QR-Code",
|
||||
"noRecoveryKeyTitle": "Kein Wiederherstellungsschlüssel?",
|
||||
"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",
|
||||
@@ -517,5 +518,6 @@
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
@@ -514,5 +515,9 @@
|
||||
"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",
|
||||
"period": "Période"
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"invalidQRCode": "二维码无效",
|
||||
"noRecoveryKeyTitle": "没有恢复密钥吗?",
|
||||
"enterEmailHint": "请输入您的电子邮件地址",
|
||||
"enterNewEmailHint": "请输入您的新电子邮件地址",
|
||||
"invalidEmailTitle": "无效的电子邮件地址",
|
||||
"invalidEmailMessage": "请输入一个有效的电子邮件地址。",
|
||||
"deleteAccount": "删除账户",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## 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.
|
||||
- .
|
||||
|
||||
|
||||
@@ -38,22 +38,23 @@
|
||||
"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.2.1",
|
||||
"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.13",
|
||||
"prettier-plugin-packagejson": "^2.5.14",
|
||||
"shx": "^0.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,11 +12,16 @@ import fs_ from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { z } from "zod";
|
||||
import type { FFmpegCommand } from "../../types/ipc";
|
||||
import log from "../log-worker";
|
||||
import { messagePortMainEndpoint } from "../utils/comlink";
|
||||
import { wait } from "../utils/common";
|
||||
import { nullToUndefined, wait } from "../utils/common";
|
||||
import { execAsyncWorker } from "../utils/exec-worker";
|
||||
import {
|
||||
authenticatedRequestHeaders,
|
||||
publicRequestHeaders,
|
||||
} from "../utils/http";
|
||||
|
||||
/* Ditto in the web app's code (used by the Wasm FFmpeg invocation). */
|
||||
const ffmpegPathPlaceholder = "FFMPEG";
|
||||
@@ -44,7 +49,9 @@ export interface FFmpegUtilityProcess {
|
||||
ffmpegGenerateHLSPlaylistAndSegments: (
|
||||
inputFilePath: string,
|
||||
outputPathPrefix: string,
|
||||
outputUploadURL: string,
|
||||
fileID: number,
|
||||
fetchURL: string,
|
||||
authToken: string,
|
||||
) => Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined>;
|
||||
|
||||
ffmpegDetermineVideoDuration: (inputFilePath: string) => Promise<number>;
|
||||
@@ -52,7 +59,11 @@ export interface FFmpegUtilityProcess {
|
||||
|
||||
log.debugString("Started ffmpeg utility 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.
|
||||
parseInitData(e.data);
|
||||
// Expose an instance of `FFmpegUtilityProcess` on the port we got from our
|
||||
// parent.
|
||||
expose(
|
||||
@@ -64,9 +75,26 @@ process.parentPort.once("message", (e) => {
|
||||
} satisfies FFmpegUtilityProcess,
|
||||
messagePortMainEndpoint(e.ports[0]!),
|
||||
);
|
||||
// Let the main process know we're ready.
|
||||
mainProcess("ack", undefined);
|
||||
});
|
||||
|
||||
/**
|
||||
* We cannot access Electron's {@link app} object within a utility process, so
|
||||
* we pass the value of `app.getVersion()` during initialization, and it can be
|
||||
* subsequently retrieved from here.
|
||||
*/
|
||||
let _desktopAppVersion: string | undefined;
|
||||
|
||||
/** Equivalent to `app.getVersion()` */
|
||||
const desktopAppVersion = () => _desktopAppVersion!;
|
||||
|
||||
const FFmpegWorkerInitData = z.object({ appVersion: z.string() });
|
||||
|
||||
const parseInitData = (data: unknown) => {
|
||||
_desktopAppVersion = FFmpegWorkerInitData.parse(data).appVersion;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a message to the main process using a barebones RPC protocol.
|
||||
*/
|
||||
@@ -184,6 +212,7 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
playlistPath: string;
|
||||
dimensions: { width: number; height: number };
|
||||
videoSize: number;
|
||||
videoObjectID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +228,7 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
*
|
||||
* 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
|
||||
* ffmpeg -i in.mov -vf "scale=-2:'min(720,ih)',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]
|
||||
*
|
||||
@@ -210,9 +239,17 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
* 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.
|
||||
* @param fileID The ID of the {@link EnteFile} whose HLS playlist we are
|
||||
* generating.
|
||||
*
|
||||
* @param fetchURL The fully resolved API URL for obtaining pre-signed S3 URLs
|
||||
* for uploading the generated video segment file.
|
||||
*
|
||||
* @param authToken A token that can be used to make API request to
|
||||
* {@link fetchURL}.
|
||||
*
|
||||
* @returns The path to the file on the user's file system containing the
|
||||
* generated HLS playlist, and other metadata about the generated video stream.
|
||||
*
|
||||
* If the video is such that it doesn't require stream generation, then this
|
||||
* function returns `undefined`.
|
||||
@@ -220,7 +257,9 @@ export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
inputFilePath: string,
|
||||
outputPathPrefix: string,
|
||||
outputUploadURL: string,
|
||||
fileID: number,
|
||||
fetchURL: string,
|
||||
authToken: string,
|
||||
): Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined> => {
|
||||
const { isH264, isHDR, bitrate } =
|
||||
await detectVideoCharacteristics(inputFilePath);
|
||||
@@ -357,11 +396,23 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
// - 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.
|
||||
//
|
||||
// [Note: ffmpeg newlines]
|
||||
//
|
||||
// Tested on Windows that ffmpeg recognizes these lines correctly. In
|
||||
// general, ffmpeg tends to expect input and write output the Unix way (\n),
|
||||
// even when we're running on Windows.
|
||||
//
|
||||
// - The ffmetadata and the HLS playlist file generated by ffmpeg uses \n
|
||||
// separators, even on Windows.
|
||||
// - The HLS key info file, expected as an input by ffmpeg, works fine when
|
||||
// \n separated even on Windows.
|
||||
//
|
||||
const keyInfo = [keyURI, keyPath].join("\n");
|
||||
|
||||
// Overview:
|
||||
//
|
||||
// - Video H.264 HD 720p 30fps.
|
||||
// - Video H.264 HD 720p (max) 30fps.
|
||||
// - Audio AAC 128kbps.
|
||||
// - Encrypted HLS playlist with a single file containing all the chunks.
|
||||
//
|
||||
@@ -399,7 +450,7 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
// keeping aspect ratio and the calculated
|
||||
// dimension divisible by 2 (some of the other
|
||||
// operations require an even pixel count).
|
||||
"scale=-2:720",
|
||||
"scale=-2:'min(720,ih)'",
|
||||
// Convert the video to a constant 30 fps,
|
||||
// duplicating or dropping frames as necessary.
|
||||
"fps=30",
|
||||
@@ -475,6 +526,7 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
|
||||
let dimensions: { width: number; height: number };
|
||||
let videoSize: number;
|
||||
let videoObjectID: string;
|
||||
|
||||
try {
|
||||
// Write the key and the keyInfo to their desired paths.
|
||||
@@ -491,6 +543,15 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
// Note: Depending on the size of the input file, this may take long!
|
||||
await execAsyncWorker(commandWithRedirection);
|
||||
|
||||
// While ffmpeg uses \n as the line separator in the generated playlist
|
||||
// file on Windows too, add an extra safety check that should fail the
|
||||
// HLS generation if this doesn't hold. See: [Note: ffmpeg newlines].
|
||||
if (process.platform == "win32") {
|
||||
const playlistText = await fs.readFile(playlistPath, "utf-8");
|
||||
if (playlistText.includes("\r\n"))
|
||||
throw new Error("Unexpected Windows newlines in playlist");
|
||||
}
|
||||
|
||||
// Determine the dimensions of the generated video from the stderr
|
||||
// output produced by ffmpeg during the conversion.
|
||||
dimensions = await detectVideoDimensions(stderrPath);
|
||||
@@ -499,7 +560,13 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
// the generated .ts file.
|
||||
videoSize = await fs.stat(videoPath).then((st) => st.size);
|
||||
|
||||
await uploadVideoSegments(videoPath, videoSize, outputUploadURL);
|
||||
videoObjectID = await uploadVideoSegments(
|
||||
videoPath,
|
||||
videoSize,
|
||||
fileID,
|
||||
fetchURL,
|
||||
authToken,
|
||||
);
|
||||
} catch (e) {
|
||||
log.error("HLS generation failed", e);
|
||||
await Promise.all([deletePathIgnoringErrors(playlistPath)]);
|
||||
@@ -515,7 +582,7 @@ const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
]);
|
||||
}
|
||||
|
||||
return { playlistPath, dimensions, videoSize };
|
||||
return { playlistPath, dimensions, videoSize, videoObjectID };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -544,10 +611,10 @@ const deletePathIgnoringErrors = async (tempFilePath: string) => {
|
||||
*
|
||||
* 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/;
|
||||
const videoStreamLineRegex = /Stream #.+: Video:(.+)\r?\n/;
|
||||
|
||||
/** {@link videoStreamLineRegex}, but global. */
|
||||
const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g;
|
||||
const videoStreamLinesRegex = /Stream #.+: Video:(.+)\r?\n/g;
|
||||
|
||||
/**
|
||||
* A regex that matches "<digits> kb/s" preceded by a space. See
|
||||
@@ -762,10 +829,10 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload the file at the given {@link videoFilePath} to the provided presigned
|
||||
* {@link objectUploadURL} using a HTTP PUT request.
|
||||
* Upload the file at the given {@link videoFilePath} to the provided pre-signed
|
||||
* URL(s) using a HTTP PUT request.
|
||||
*
|
||||
* In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff.
|
||||
* All HTTP requests are retried up to 3 times with exponential backoff.
|
||||
*
|
||||
* See: [Note: Upload HLS video segment from node side].
|
||||
*
|
||||
@@ -774,11 +841,146 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => {
|
||||
*
|
||||
* @param videoSize The size in bytes of the file at {@link videoFilePath}.
|
||||
*
|
||||
* @param objectUploadURL A pre-signed URL to upload the file.
|
||||
* @param fileID The ID of the {@link EnteFile} whose video segment this is.
|
||||
*
|
||||
* ---
|
||||
* @param fetchURL The API URL for fetching pre-signed upload URLs.
|
||||
*
|
||||
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx`
|
||||
* @param authToken The user's auth token for use with {@link fetchURL}.
|
||||
*
|
||||
* @return The object ID of the uploaded file on remote storage.
|
||||
*/
|
||||
const uploadVideoSegments = async (
|
||||
videoFilePath: string,
|
||||
videoSize: number,
|
||||
fileID: number,
|
||||
fetchURL: string,
|
||||
authToken: string,
|
||||
) => {
|
||||
// Self hosters might be using Cloudflare's free plan which (currently) has
|
||||
// a maximum request size of 100 MB. Keeping a bit of margin for headers,
|
||||
const partSize = 96 * 1024 * 1024; /* 96 MB */
|
||||
const partCount = Math.ceil(videoSize / partSize);
|
||||
|
||||
const { objectID, url, partURLs, completeURL } =
|
||||
await getFilePreviewDataUploadURL(
|
||||
partCount,
|
||||
fileID,
|
||||
fetchURL,
|
||||
authToken,
|
||||
);
|
||||
|
||||
if (url) {
|
||||
await uploadVideoSegmentsSingle(videoFilePath, videoSize, url);
|
||||
} else if (partURLs && completeURL) {
|
||||
await uploadVideoSegmentsMultipart(
|
||||
videoFilePath,
|
||||
videoSize,
|
||||
partSize,
|
||||
partURLs,
|
||||
completeURL,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Malformed upload URLs");
|
||||
}
|
||||
|
||||
return objectID;
|
||||
};
|
||||
|
||||
const FilePreviewDataUploadURLResponse = z.object({
|
||||
/**
|
||||
* The objectID with which this uploaded data can be referred to post upload
|
||||
* (e.g. when invoking {@link putVideoData}).
|
||||
*/
|
||||
objectID: z.string(),
|
||||
/**
|
||||
* A pre-signed URL that can be used to upload the file.
|
||||
*
|
||||
* This will be present only if we requested a singular object upload URL.
|
||||
*/
|
||||
url: z.string().nullish().transform(nullToUndefined),
|
||||
/**
|
||||
* A list of pre-signed URLs that can be used to upload parts of a multipart
|
||||
* upload of the uploaded data.
|
||||
*
|
||||
* This will be present only if we requested a multipart upload URLs for the
|
||||
* object by setting `isMultiPart` true in the request.
|
||||
*/
|
||||
partURLs: z.string().array().nullish().transform(nullToUndefined),
|
||||
/**
|
||||
* A pre-signed URL that can be used to finalize the multipart upload.
|
||||
*
|
||||
* This will be present only if we requested a multipart upload URLs for the
|
||||
* object by setting `isMultiPart` true in the request.
|
||||
*/
|
||||
completeURL: z.string().nullish().transform(nullToUndefined),
|
||||
});
|
||||
|
||||
/**
|
||||
* Obtain a pre-signed URL(s) that can be used to upload the "file preview data"
|
||||
* of type "vid_preview".
|
||||
*
|
||||
* This will be the file containing the encrypted video segments which the
|
||||
* "vid_preview" HLS playlist for the file would refer to.
|
||||
*
|
||||
* @param partCount If greater than 1, then we request for a multipart upload.
|
||||
*/
|
||||
export const getFilePreviewDataUploadURL = async (
|
||||
partCount: number,
|
||||
fileID: number,
|
||||
fetchURL: string,
|
||||
authToken: string,
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
fileID: fileID.toString(),
|
||||
type: "vid_preview",
|
||||
});
|
||||
if (partCount > 1) {
|
||||
params.set("isMultiPart", "true");
|
||||
params.set("count", partCount.toString());
|
||||
}
|
||||
|
||||
const res = await retryEnsuringHTTPOk(() =>
|
||||
fetch(`${fetchURL}?${params.toString()}`, {
|
||||
headers: authenticatedRequestHeaders(
|
||||
desktopAppVersion(),
|
||||
authToken,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
return FilePreviewDataUploadURLResponse.parse(await res.json());
|
||||
};
|
||||
|
||||
const uploadVideoSegmentsSingle = (
|
||||
videoFilePath: string,
|
||||
videoSize: number,
|
||||
objectUploadURL: string,
|
||||
) =>
|
||||
retryEnsuringHTTPOk(() =>
|
||||
// net.fetch is 40-50x slower than the native fetch for this particular
|
||||
// PUT request. This is easily reproducible - replace `fetch` with
|
||||
// `net.fetch`, then even on localhost the PUT requests start taking a
|
||||
// minute or so, while they take second(s) with node's native fetch.
|
||||
fetch(objectUploadURL, {
|
||||
method: "PUT",
|
||||
// net.fetch deduces and inserts a content-length for us, when we
|
||||
// use the node native fetch then we need to provide it explicitly.
|
||||
headers: {
|
||||
...publicRequestHeaders(desktopAppVersion()),
|
||||
"Content-Length": `${videoSize}`,
|
||||
},
|
||||
// See: [Note: duplex param required for stream body]
|
||||
// @ts-expect-error ^see note above
|
||||
duplex: "half",
|
||||
body: Readable.toWeb(fs_.createReadStream(videoFilePath)),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Retry a async operation on failure up to 3 times (1 original + 2 retries)
|
||||
* with exponential backoff.
|
||||
*
|
||||
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOk`
|
||||
* from `web/packages/base/http.ts`
|
||||
*
|
||||
* - We don't have the rest of the scaffolding used by that function, which is
|
||||
@@ -795,65 +997,84 @@ const pseudoFFProbeVideo = async (inputFilePath: string) => {
|
||||
* - This also moved to a utility process, where we also have a more restricted
|
||||
* ability to import electron API.
|
||||
*/
|
||||
const uploadVideoSegments = async (
|
||||
videoFilePath: string,
|
||||
videoSize: number,
|
||||
objectUploadURL: string,
|
||||
) => {
|
||||
const waitTimeBeforeNextTry = [5000, 20000];
|
||||
const retryEnsuringHTTPOk = async (request: () => Promise<Response>) => {
|
||||
const waitTimeBeforeNextTry = [10000, 30000];
|
||||
|
||||
while (true) {
|
||||
let abort = false;
|
||||
try {
|
||||
const nodeStream = fs_.createReadStream(videoFilePath);
|
||||
const webStream = Readable.toWeb(nodeStream);
|
||||
|
||||
// net.fetch is 40-50x slower than the native fetch for this
|
||||
// particular PUT request. This is easily reproducible - replace
|
||||
// `fetch` with `net.fetch`, then even on localhost the PUT requests
|
||||
// start taking a minute or so, while they take second(s) with
|
||||
// node's native fetch.
|
||||
const res = await fetch(objectUploadURL, {
|
||||
method: "PUT",
|
||||
// net.fetch apparently deduces and inserts a content-length,
|
||||
// because when we use the node native fetch then we need to
|
||||
// provide it explicitly.
|
||||
headers: { "Content-Length": `${videoSize}` },
|
||||
// The duplex option is required since we're passing a stream.
|
||||
//
|
||||
// @ts-expect-error TypeScript's libdom.d.ts does not include
|
||||
// the "duplex" parameter, e.g. see
|
||||
// https://github.com/node-fetch/node-fetch/issues/1769.
|
||||
duplex: "half",
|
||||
body: webStream,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Success.
|
||||
return;
|
||||
}
|
||||
if (res.status >= 400 && res.status < 500 && res.status != 429) {
|
||||
// HTTP 4xx, except potentially transient 429 rate limits.
|
||||
abort = true;
|
||||
}
|
||||
const res = await request();
|
||||
if (res.ok) /* Success. */ return res;
|
||||
throw new Error(
|
||||
`Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`,
|
||||
`Request failed: 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);
|
||||
}
|
||||
await wait(t);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadVideoSegmentsMultipart = async (
|
||||
videoFilePath: string,
|
||||
videoSize: number,
|
||||
partSize: number,
|
||||
partUploadURLs: string[],
|
||||
completionURL: string,
|
||||
) => {
|
||||
// The part we're currently uploading.
|
||||
let partNumber = 0;
|
||||
// A rolling offset into the file.
|
||||
let start = 0;
|
||||
// See `createMultipartUploadRequestBody` in the web code for a more
|
||||
// expansive and documented version of this XML body construction.
|
||||
const completionXML = ["<CompleteMultipartUpload>"];
|
||||
for (const partUploadURL of partUploadURLs) {
|
||||
partNumber += 1;
|
||||
const size = Math.min(start + partSize, videoSize) - start;
|
||||
const end = start + size - 1;
|
||||
const res = await retryEnsuringHTTPOk(() =>
|
||||
fetch(partUploadURL, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...publicRequestHeaders(desktopAppVersion()),
|
||||
"Content-Length": `${size}`,
|
||||
},
|
||||
// See: [Note: duplex param required for stream body]
|
||||
// @ts-expect-error ^see note above
|
||||
duplex: "half",
|
||||
body: Readable.toWeb(
|
||||
// start and end are inclusive 0-indexed range of bytes to
|
||||
// read from the file.
|
||||
fs_.createReadStream(videoFilePath, { start, end }),
|
||||
),
|
||||
}),
|
||||
);
|
||||
const eTag = res.headers.get("etag");
|
||||
if (!eTag) throw new Error("Response did not have an ETag");
|
||||
start += size;
|
||||
completionXML.push(
|
||||
`<Part><PartNumber>${partNumber}</PartNumber><ETag>${eTag}</ETag></Part>`,
|
||||
);
|
||||
}
|
||||
completionXML.push("</CompleteMultipartUpload>");
|
||||
const completionBody = completionXML.join("");
|
||||
return await retryEnsuringHTTPOk(() =>
|
||||
fetch(completionURL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...publicRequestHeaders(desktopAppVersion()),
|
||||
"Content-Type": "text/xml",
|
||||
},
|
||||
body: completionBody,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A regex that matches the first line of the form
|
||||
*
|
||||
|
||||
@@ -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,6 +15,7 @@ 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";
|
||||
@@ -23,6 +24,8 @@ import { fsStatMtime } from "./fs";
|
||||
|
||||
log.debugString("Started ML utility 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.
|
||||
parseInitData(e.data);
|
||||
@@ -50,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.error("Unparseable initialization data");
|
||||
}
|
||||
_userDataPath = MLWorkerInitData.parse(data).userDataPath;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,9 +15,22 @@ 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).
|
||||
@@ -92,18 +105,22 @@ export const triggerCreateUtilityProcess = (
|
||||
window: BrowserWindow,
|
||||
) => triggerCreateMLUtilityProcess(window);
|
||||
|
||||
export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => {
|
||||
const terminateMLProcessIfRunning = () => {
|
||||
if (_utilityProcessML) {
|
||||
log.debug(() => "Terminating previous ML utility process");
|
||||
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({ userDataPath }, [port1]);
|
||||
child.postMessage(/* MLWorkerInitData */ { userDataPath }, [port1]);
|
||||
|
||||
window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]);
|
||||
|
||||
@@ -173,7 +190,20 @@ const handleMessagesFromMLUtilityProcess = (child: UtilityProcess) => {
|
||||
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));
|
||||
@@ -182,8 +212,10 @@ const createFFmpegUtilityProcessEndpoint = () => {
|
||||
|
||||
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. The utility process will reply with an "ack" when it get it.
|
||||
child.postMessage({}, [port1]);
|
||||
// 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) {
|
||||
@@ -201,6 +233,8 @@ const createFFmpegUtilityProcessEndpoint = () => {
|
||||
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;
|
||||
|
||||
@@ -277,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
|
||||
@@ -289,8 +289,10 @@ 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 makeFileForStreamOrPathOrZipItem>[0];
|
||||
const path = params.get("path");
|
||||
@@ -324,7 +326,9 @@ const handleGenerateHLSWrite = async (
|
||||
result = await worker.ffmpegGenerateHLSPlaylistAndSegments(
|
||||
inputFilePath,
|
||||
outputFilePathPrefix,
|
||||
objectUploadURL,
|
||||
fileID,
|
||||
fetchURL,
|
||||
authToken,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
@@ -332,13 +336,18 @@ const handleGenerateHLSWrite = async (
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const { playlistPath, videoSize, dimensions } = result;
|
||||
const { playlistPath, dimensions, videoSize, videoObjectID } = result;
|
||||
|
||||
const playlistToken = randomUUID();
|
||||
pendingVideoResults.set(playlistToken, playlistPath);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ playlistToken, videoSize, dimensions }),
|
||||
JSON.stringify({
|
||||
playlistToken,
|
||||
dimensions,
|
||||
videoSize,
|
||||
videoObjectID,
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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,
|
||||
});
|
||||
@@ -184,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"
|
||||
@@ -297,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"
|
||||
@@ -1234,10 +1234,10 @@ electron-updater@^6.6.3:
|
||||
semver "^7.6.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^36.2.1:
|
||||
version "36.2.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.2.1.tgz#7c9d7c6c7076a3ddb7b1c326a851cb92c775b0b1"
|
||||
integrity sha512-mm1Y+Ms46xcOTA69h8hpqfX392HfV4lga9aEkYkd/Syx1JBStvcACOIouCgGrnZpxNZPVS1jM8NTcMkNjuK6BQ==
|
||||
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"
|
||||
@@ -2664,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.13:
|
||||
version "2.5.13"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.13.tgz#4e908d25b97a8f8f1cb69fd611caa677653787d5"
|
||||
integrity sha512-94B/Xy25HwiwSkGUGnwQw3cBw9jg9L5LfKCHpuRMjC8ETPq4oCMa2S4EblV628E0XER9n6v5rH0TQY9cUd10pg==
|
||||
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 "3.2.1"
|
||||
synckit "0.11.5"
|
||||
synckit "0.11.6"
|
||||
|
||||
prettier@3.5.3:
|
||||
version "3.5.3"
|
||||
@@ -3108,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.11.5:
|
||||
version "0.11.5"
|
||||
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.5.tgz#258b3185736512f7ef11a42d67c4f3ad49da1744"
|
||||
integrity sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==
|
||||
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.2.4"
|
||||
tslib "^2.8.1"
|
||||
|
||||
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
|
||||
version "6.2.1"
|
||||
@@ -3214,11 +3213,6 @@ tslib@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
|
||||
tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
@@ -3410,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==
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -49,10 +49,10 @@ 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
|
||||
```
|
||||
|
||||
>[!IMPORTANT]
|
||||
@@ -74,4 +74,4 @@ functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc.
|
||||
|
||||
## References
|
||||
|
||||
- [Environment variables and ports](/self-hosting/faq/environment)
|
||||
- [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.
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 145 KiB |
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/widget_container">
|
||||
|
||||
<!-- Main Image (if available) -->
|
||||
<ImageView
|
||||
android:id="@+id/widget_img"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" /> <!-- Initially hidden -->
|
||||
|
||||
<!-- Gradient Overlay for Text Readability -->
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:background="@layout/gradient_overlay"
|
||||
android:visibility="gone"> <!-- Initially hidden, shown when image is available -->
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/widget_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Placeholder View (when no image available) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_placeholder_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/widget_placeholder_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_placeholder"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_albums_widget"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_placeholder_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to Settings -> General to customise the widget"
|
||||
android:textSize="12sp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/widget_text_color"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:layout_marginTop="12dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -65,15 +65,15 @@
|
||||
android:id="@+id/widget_placeholder"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_home_widget_default"
|
||||
android:src="@drawable/ic_memories_widget"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_placeholder_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Your memories will appear here"
|
||||
android:textSize="14sp"
|
||||
android:text="Go to Settings -> General to customise the widget"
|
||||
android:textSize="12sp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/widget_text_color"
|
||||
android:paddingStart="8dp"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/widget_container">
|
||||
|
||||
<!-- Main Image (if available) -->
|
||||
<ImageView
|
||||
android:id="@+id/widget_img"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone" /> <!-- Initially hidden -->
|
||||
|
||||
<!-- Gradient Overlay for Text Readability -->
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:background="@layout/gradient_overlay"
|
||||
android:visibility="gone"> <!-- Initially hidden, shown when image is available -->
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:id="@+id/widget_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
android:id="@+id/widget_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:visibility="gone"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Placeholder View (when no image available) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_placeholder_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/widget_placeholder_bg">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_placeholder"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:src="@drawable/ic_people_widget"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_placeholder_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to Settings -> General to customise the widget"
|
||||
android:textSize="12sp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/widget_text_color"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:layout_marginTop="12dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
10
mobile/android/app/src/main/res/xml/albums_widget.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="100dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="900000"
|
||||
android:initialLayout="@layout/albums_widget_layout"
|
||||
android:previewImage="@drawable/albums_widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" path="."/>
|
||||
<exclude domain="file" path="."/>
|
||||
<exclude domain="database" path="."/>
|
||||
<exclude domain="sharedpref" path="."/>
|
||||
<exclude domain="external" path="."/>
|
||||
</cloud-backup>
|
||||
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="."/>
|
||||
<exclude domain="file" path="."/>
|
||||
<exclude domain="database" path="."/>
|
||||
<exclude domain="sharedpref" path="."/>
|
||||
<exclude domain="external" path="."/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,14 @@
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="true">ente.io</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
10
mobile/android/app/src/main/res/xml/people_widget.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="100dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="900000"
|
||||
android:initialLayout="@layout/people_widget_layout"
|
||||
android:previewImage="@drawable/people_widget_preview"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen">
|
||||
</appwidget-provider>
|
||||
BIN
mobile/assets/2.0x/albums-widget-static.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
mobile/assets/2.0x/memories-widget-static.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
mobile/assets/2.0x/people-widget-static.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
mobile/assets/3.0x/albums-widget-static.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
mobile/assets/3.0x/memories-widget-static.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
mobile/assets/3.0x/people-widget-static.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
mobile/assets/albums-widget-static.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
3
mobile/assets/icons/albums-widget-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.668 3.33073V13.3307H6.66797V3.33073H16.668ZM16.668 1.66406H6.66797C5.7513 1.66406 5.0013 2.41406 5.0013 3.33073V13.3307C5.0013 14.2474 5.7513 14.9974 6.66797 14.9974H16.668C17.5846 14.9974 18.3346 14.2474 18.3346 13.3307V3.33073C18.3346 2.41406 17.5846 1.66406 16.668 1.66406ZM9.58464 9.7224L10.993 11.6057L13.0596 9.0224L15.8346 12.4974H7.5013L9.58464 9.7224ZM1.66797 4.9974V16.6641C1.66797 17.5807 2.41797 18.3307 3.33464 18.3307H15.0013V16.6641H3.33464V4.9974H1.66797Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
4
mobile/assets/icons/memories-widget-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="1" width="15" height="18" rx="1.4" stroke="black" stroke-width="1.4"/>
|
||||
<path d="M11.773 16.5745C11.5071 16.8091 11.0978 16.8091 10.8319 16.5711L10.7934 16.5371C8.95659 14.9221 7.75655 13.8646 7.80203 12.5454C7.82302 11.9674 8.12741 11.4132 8.62072 11.0868C9.54436 10.4748 10.6849 10.7604 11.3007 11.4608C11.9164 10.7604 13.057 10.4714 13.9807 11.0868C14.474 11.4132 14.7783 11.9674 14.7993 12.5454C14.8483 13.8646 13.6448 14.9221 11.808 16.5439L11.773 16.5745Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
3
mobile/assets/icons/past-year-memory-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.3333 3.33073H15.5V1.66406H13.8333V3.33073H7.16667V1.66406H5.5V3.33073H4.66667C3.74167 3.33073 3 4.08073 3 4.9974V16.6641C3 17.5807 3.74167 18.3307 4.66667 18.3307H16.3333C17.25 18.3307 18 17.5807 18 16.6641V4.9974C18 4.08073 17.25 3.33073 16.3333 3.33073ZM16.3333 16.6641H4.66667V8.33073H16.3333V16.6641ZM16.3333 6.66406H4.66667V4.9974H16.3333V6.66406ZM6.33333 9.9974H10.5V14.1641H6.33333V9.9974Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
3
mobile/assets/icons/people-widget-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.9987 5.0026C10.9154 5.0026 11.6654 5.7526 11.6654 6.66927C11.6654 7.58594 10.9154 8.33594 9.9987 8.33594C9.08203 8.33594 8.33203 7.58594 8.33203 6.66927C8.33203 5.7526 9.08203 5.0026 9.9987 5.0026ZM9.9987 13.3359C12.2487 13.3359 14.832 14.4109 14.9987 15.0026H4.9987C5.19036 14.4026 7.75703 13.3359 9.9987 13.3359ZM9.9987 3.33594C8.15703 3.33594 6.66536 4.8276 6.66536 6.66927C6.66536 8.51094 8.15703 10.0026 9.9987 10.0026C11.8404 10.0026 13.332 8.51094 13.332 6.66927C13.332 4.8276 11.8404 3.33594 9.9987 3.33594ZM9.9987 11.6693C7.7737 11.6693 3.33203 12.7859 3.33203 15.0026V16.6693H16.6654V15.0026C16.6654 12.7859 12.2237 11.6693 9.9987 11.6693Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 781 B |
11
mobile/assets/icons/smart-memory-icon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_38630_146929)">
|
||||
<path d="M11.1613 17.0685C10.5815 17.5949 9.68898 17.5949 9.10919 17.0609L9.02528 16.9846C5.0202 13.3609 2.40355 10.9884 2.50272 8.02846C2.5485 6.73158 3.2122 5.4881 4.28784 4.75574C6.30183 3.38257 8.78879 4.02339 10.1314 5.5949C11.4741 4.02339 13.9611 3.37494 15.975 4.75574C17.0507 5.4881 17.7144 6.73158 17.7602 8.02846C17.867 10.9884 15.2427 13.3609 11.2376 16.9998L11.1613 17.0685Z" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M14.5938 0.34668C14.9459 -0.282495 15.8744 -0.282502 16.2266 0.34668L16.293 0.491211L16.9727 2.34277L18.8252 3.02344L18.9697 3.08984C19.5989 3.44199 19.5989 4.37051 18.9697 4.72266L18.8252 4.78906L16.9727 5.46875L16.293 7.32129C16.01 8.09135 14.971 8.13983 14.5938 7.46582L14.5273 7.32129L13.8467 5.46875L11.9951 4.78906C11.1738 4.4873 11.1738 3.3252 11.9951 3.02344L13.8467 2.34277L14.5273 0.491211L14.5938 0.34668Z" fill="black" stroke="#F5F5F5" stroke-width="1.25366"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_38630_146929">
|
||||
<rect width="20" height="20" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
mobile/assets/memories-widget-static.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
mobile/assets/people-widget-static.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
mobile/ios/EnteAlbumWidget/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
277
mobile/ios/EnteAlbumWidget/EnteAlbumWidget.swift
Normal file
17
mobile/ios/EnteAlbumWidget/EnteAlbumWidgetBundle.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// EnteAlbumWidgetBundle.swift
|
||||
// EnteAlbumWidget
|
||||
//
|
||||
// Created by Prateek Sunal on 5/15/25.
|
||||
// Copyright © 2025 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct EnteAlbumWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
EnteAlbumWidget()
|
||||
}
|
||||
}
|
||||
11
mobile/ios/EnteAlbumWidget/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
mobile/ios/EnteAlbumWidgetExtension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.io.ente.frame.EnteMemoryWidget</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
277
mobile/ios/EntePeopleWidget/EntePeopleWidget.swift
Normal file
17
mobile/ios/EntePeopleWidget/EntePeopleWidgetBundle.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// EntePeopleWidgetBundle.swift
|
||||
// EntePeopleWidget
|
||||
//
|
||||
// Created by Prateek Sunal on 5/15/25.
|
||||
// Copyright © 2025 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct EntePeopleWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
EntePeopleWidget()
|
||||
}
|
||||
}
|
||||
11
mobile/ios/EntePeopleWidget/Info.plist
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||