Compare commits
185 Commits
mob_6_marc
...
docs-addiO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acf978c302 | ||
|
|
26c35d997a | ||
|
|
85d6552943 | ||
|
|
56876e4a28 | ||
|
|
738128a7c5 | ||
|
|
eab1d98d2d | ||
|
|
7aa4f5bb16 | ||
|
|
27fa57f608 | ||
|
|
f135b65d31 | ||
|
|
9b09ebe3f0 | ||
|
|
9e64752677 | ||
|
|
563d65cc1d | ||
|
|
68132147e7 | ||
|
|
6979617d12 | ||
|
|
c4812abab3 | ||
|
|
f09ef7ae10 | ||
|
|
062bbdfa88 | ||
|
|
1f87ef8cb7 | ||
|
|
df14f18881 | ||
|
|
c4d8ddbf26 | ||
|
|
9132be591d | ||
|
|
2fa555163c | ||
|
|
643da1491a | ||
|
|
3b568bf914 | ||
|
|
fd325d0be5 | ||
|
|
8e158677f2 | ||
|
|
d8490ea4b1 | ||
|
|
01e258557c | ||
|
|
f9dbf0efea | ||
|
|
51ef7c60fa | ||
|
|
600736e70f | ||
|
|
ef3ccbd91b | ||
|
|
55a68f9d29 | ||
|
|
5918698366 | ||
|
|
d0b58b75c8 | ||
|
|
a72eb78e53 | ||
|
|
8d07b16e09 | ||
|
|
caadba3996 | ||
|
|
427cc9d414 | ||
|
|
d8995ef375 | ||
|
|
c3831230e0 | ||
|
|
76d8038899 | ||
|
|
ad0169b7e5 | ||
|
|
76887b2205 | ||
|
|
7249b25180 | ||
|
|
ffd2a55ca0 | ||
|
|
eaf576967b | ||
|
|
e6a9ccefe7 | ||
|
|
d0b25b31c8 | ||
|
|
3211e6afe6 | ||
|
|
a7cc5e7165 | ||
|
|
bea32ac7e3 | ||
|
|
3be7f7b55e | ||
|
|
4a833e0799 | ||
|
|
10a9ad02f8 | ||
|
|
ba79588090 | ||
|
|
3593a8e545 | ||
|
|
643a6cf413 | ||
|
|
dbb14f0a24 | ||
|
|
cba6676bb5 | ||
|
|
d43cf1fb86 | ||
|
|
f02974045b | ||
|
|
20268c236a | ||
|
|
0b7aa97db1 | ||
|
|
9a39298acd | ||
|
|
36e1e758c5 | ||
|
|
f74f13c7a8 | ||
|
|
eb9e61579e | ||
|
|
300b3c89a3 | ||
|
|
302d2af3d2 | ||
|
|
3feee66d3a | ||
|
|
b953d6d513 | ||
|
|
d88b39ec46 | ||
|
|
145e025eea | ||
|
|
13c36d9c40 | ||
|
|
dd807368b2 | ||
|
|
0f0790df5f | ||
|
|
9d2e1cd502 | ||
|
|
a640a430bf | ||
|
|
26cb6ad722 | ||
|
|
b114dd54b9 | ||
|
|
b8e3d88575 | ||
|
|
b3d5731731 | ||
|
|
84b880d7cf | ||
|
|
3900ee609f | ||
|
|
0ee496401a | ||
|
|
511c324bad | ||
|
|
51f2868f98 | ||
|
|
2a70327153 | ||
|
|
f29341ccb2 | ||
|
|
89b35f44c3 | ||
|
|
beeafe4aa6 | ||
|
|
4b631aa423 | ||
|
|
83729aced4 | ||
|
|
bbcfa865d1 | ||
|
|
4bf629a44c | ||
|
|
4e417e9490 | ||
|
|
b6aefd1845 | ||
|
|
a68f1e91c5 | ||
|
|
a3e8d3c1a3 | ||
|
|
15473d80d8 | ||
|
|
fa349caf0c | ||
|
|
920e26255c | ||
|
|
2a3466da63 | ||
|
|
c59da52f71 | ||
|
|
aa551463b3 | ||
|
|
949909631a | ||
|
|
de2b399941 | ||
|
|
6685c68c35 | ||
|
|
02f1ac4f2f | ||
|
|
573cc787e5 | ||
|
|
997b87bd26 | ||
|
|
ef013473fc | ||
|
|
df96f42a61 | ||
|
|
a144d39a47 | ||
|
|
70c98b8877 | ||
|
|
2a5f774423 | ||
|
|
4796d8a54a | ||
|
|
694a8a46dd | ||
|
|
61809889e9 | ||
|
|
981716fbcb | ||
|
|
be25081a73 | ||
|
|
8e3e741b1a | ||
|
|
a056cfd154 | ||
|
|
98987326e2 | ||
|
|
b9de012c28 | ||
|
|
50adfa7399 | ||
|
|
2d005a7d07 | ||
|
|
4faf938fbd | ||
|
|
3bb92e10e4 | ||
|
|
9f51c2ddae | ||
|
|
2a453ee321 | ||
|
|
a48505205e | ||
|
|
6697cca571 | ||
|
|
bfc0f785bc | ||
|
|
f3cc4f6fa0 | ||
|
|
781de2b60b | ||
|
|
dfe892b54e | ||
|
|
fce9c6d01e | ||
|
|
183000526c | ||
|
|
0b50d43d53 | ||
|
|
f48d97112c | ||
|
|
815009da9b | ||
|
|
e9e0b31b8a | ||
|
|
b72f65d44c | ||
|
|
5649ee7c03 | ||
|
|
74f301e936 | ||
|
|
03df527fb7 | ||
|
|
6c9887613b | ||
|
|
ca7ee5e147 | ||
|
|
c8dc9c9f46 | ||
|
|
7eaedfe138 | ||
|
|
30b23e6c3b | ||
|
|
b578c8f0de | ||
|
|
ba95d08cdd | ||
|
|
63faa29cd4 | ||
|
|
27ad9840d0 | ||
|
|
c96f2495ed | ||
|
|
b1c680cccd | ||
|
|
f487e64569 | ||
|
|
ae4e189848 | ||
|
|
5ab8169cd9 | ||
|
|
f52b6256b5 | ||
|
|
c03f63d2b2 | ||
|
|
e2aea63276 | ||
|
|
f590a43159 | ||
|
|
bc72ec1982 | ||
|
|
7050ba5f22 | ||
|
|
2e2cc7f3e7 | ||
|
|
2278b1f40e | ||
|
|
69852e436a | ||
|
|
3fe47dd4c4 | ||
|
|
5ff494320c | ||
|
|
d49f9cc054 | ||
|
|
c432125113 | ||
|
|
d25e81282d | ||
|
|
2d30ac4c46 | ||
|
|
49fe5f41e0 | ||
|
|
2aa953d5b6 | ||
|
|
b35cd47c8a | ||
|
|
24759a3923 | ||
|
|
1fba250f74 | ||
|
|
8099cbd990 | ||
|
|
b1ed3a6302 | ||
|
|
3a955f2b04 |
2
.github/workflows/web-deploy-one.yml
vendored
@@ -26,8 +26,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/web-deploy-preview.yml
vendored
@@ -26,8 +26,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
1
.github/workflows/web-deploy-staging.yml
vendored
@@ -34,7 +34,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.select-branch.outputs.branch }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
2
.github/workflows/web-deploy.yml
vendored
@@ -30,8 +30,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
4
.gitmodules
vendored
@@ -9,7 +9,3 @@
|
||||
[submodule "auth/assets/simple-icons"]
|
||||
path = auth/assets/simple-icons
|
||||
url = https://github.com/simple-icons/simple-icons.git
|
||||
[submodule "web/apps/photos/thirdparty/photoswipe"]
|
||||
path = web/apps/photos/thirdparty/photoswipe
|
||||
url = https://github.com/ente-io/PhotoSwipe.git
|
||||
branch = single-thread
|
||||
|
||||
@@ -95,8 +95,8 @@ please see our [support guide](SUPPORT.md).
|
||||
<img src=".github/assets/ente-ducky.png" width=200 alt="Ente's Mascot, Ducky,
|
||||
inviting people to Ente's source code repository" />
|
||||
|
||||
Please visit our [community page](https://ente.io/community) for all the ways to
|
||||
connect with the community.
|
||||
Please visit the [community section](https://ente.io/about#community) for all the ways to
|
||||
connect with our community.
|
||||
|
||||
[](https://discord.gg/z2YVKkycX3)
|
||||
[](https://ente.io/blog/rss.xml)
|
||||
|
||||
@@ -379,6 +379,14 @@
|
||||
{
|
||||
"title": "Fastmail"
|
||||
},
|
||||
{
|
||||
"title": "Federal Student Aid",
|
||||
"slug": "federal_student_aid",
|
||||
"altNames": [
|
||||
"FSA",
|
||||
"FAFSA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fidelity",
|
||||
"slug": "fidelity",
|
||||
@@ -483,6 +491,9 @@
|
||||
"title": "IceDrive",
|
||||
"slug": "ice_drive"
|
||||
},
|
||||
{
|
||||
"title": "ICONOMI"
|
||||
},
|
||||
{
|
||||
"title": "ID.me",
|
||||
"slug": "id_me"
|
||||
@@ -593,6 +604,11 @@
|
||||
{
|
||||
"title": "Letterboxd"
|
||||
},
|
||||
{
|
||||
"title": "LinkedIn",
|
||||
"slug": "linkedin",
|
||||
"hex": "2596be"
|
||||
},
|
||||
{
|
||||
"title": "Linux.Do",
|
||||
"slug": "linux_do",
|
||||
@@ -614,6 +630,14 @@
|
||||
"title": "Login.gov",
|
||||
"slug": "login_gov"
|
||||
},
|
||||
{
|
||||
"title": "Luma",
|
||||
"slug": "luma",
|
||||
"altNames": [
|
||||
"luma",
|
||||
"lu.ma"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Marketplace.tf",
|
||||
"slug": "marketplacedottf"
|
||||
@@ -643,6 +667,9 @@
|
||||
"MercadoLivre"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MEXC"
|
||||
},
|
||||
{
|
||||
"title": "microsoft"
|
||||
},
|
||||
@@ -952,6 +979,10 @@
|
||||
{
|
||||
"title": "RuneMate"
|
||||
},
|
||||
{
|
||||
"title": "RuneScape Wiki",
|
||||
"slug": "runescape_wiki"
|
||||
},
|
||||
{
|
||||
"title": "Rust Language Forum",
|
||||
"slug": "rust_language_forum",
|
||||
|
||||
6
auth/assets/custom-icons/icons/federal_student_aid.svg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
1
auth/assets/custom-icons/icons/iconomi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 35 30" version="1.1" style="zoom: 16;" visibility="visible"><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" visibility="visible"><g id="Group-8"><g id="Group-7" fill="#3A79F2"><rect id="Rectangle-Copy-20" x="9" y="0" width="6" height="30" rx="3"></rect><rect id="Rectangle-Copy-21" x="27" y="12" width="6" height="9" rx="3"></rect><rect id="Rectangle-Copy-22" x="18" y="12" width="6" height="18" rx="3" visibility="visible"></rect><rect id="Rectangle-Copy-23" x="0" y="21" width="6" height="9" rx="3" visibility="visible"></rect><circle id="Oval-Copy-13" cx="21" cy="6" r="3" visibility="visible"></circle></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 750 B |
1
auth/assets/custom-icons/icons/linkedin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg height="72" viewBox="0 0 72 72" width="72" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z" fill="#007EBB"/><path d="M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z" fill="#FFF"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
auth/assets/custom-icons/icons/luma.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
|
||||
<path
|
||||
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
15
auth/assets/custom-icons/icons/mexc.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns:xodm="http://www.corel.com/coreldraw/odm/2003" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 450 2500 1650" style="enable-background:new 0 0 2500 2500;" xml:space="preserve">
|
||||
<rect y="250" width="2500" height="1650" style="fill:none;"></rect>
|
||||
<g id="_2500406570000">
|
||||
<path d="M2459.7,1566.6l-540.6-937.7c-118.5-195.5-407.5-197.5-521.9,8.3l-567.6,975.2c-106,178.8,25,403.3,237.1,403.3H2204C2418.1,2015.7,2578.2,1784.9,2459.7,1566.6z" style="fill:#3156AA;"></path>
|
||||
<path d="M1680,1639.4l-33.3-58.2c-31.2-54.1-99.8-170.5-99.8-170.5l-457.4-794.3C971,439.7,690.3,425.1,571.8,647.6L39.5,1568.7c-110.2,193.4,20.8,444.9,259.9,447h1131.1h482.4h286.9C1906.7,2017.8,1813.1,1866,1680,1639.4L1680,1639.4z" style="fill:#1972E2;"></path>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="703.637" y1="1211.6566" x2="1935.647" y2="727.2267" gradientTransform="matrix(1 0 0 -1 0 2497.8899)">
|
||||
<stop offset="0" style="stop-color:#264CA2;stop-opacity:0;"></stop>
|
||||
<stop offset="1" style="stop-color:#234588;"></stop>
|
||||
</linearGradient>
|
||||
|
||||
<path d="M1680.1,1639.4l-33.3-58.2c-31.2-54.1-99.8-170.5-99.8-170.5l-295.3-519.8l-424.2,723.6c-106,178.8,25,403.4,237,403.4h363.9h482.4h289C1904.6,2015.7,1813.1,1866,1680.1,1639.4L1680.1,1639.4z" style="fill:url(#SVGID_1_);"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
7
auth/assets/custom-icons/icons/runescape_wiki.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 35 35">
|
||||
<g fill="#438ab5" fill-rule="evenodd" transform="translate(4 1)">
|
||||
<path d="M11.7311354 23.0557769L9.91249858 23.0557769 8.79846449 26.6069057 6.97030597 23 6.79891611 23 4.98027925 26.6347942 3.85672349 23.0557769 2 23.0557769 4.54228294 29.9814077 4.75175943 30 6.86556772 25.9189907 9.00794098 30 9.22693914 29.9814077 11.7311354 23.0557769zM14.3322795 29.8698539L14.3322795 23.0557769 12.7135975 23.0557769 12.7135975 29.8698539 14.3322795 29.8698539zM22.2084227 29.8698539L19.1900568 26.3001328 22.0560762 23.0557769 20.1422227 23.0557769 17.6951564 26.0212483 17.6951564 23.0557769 16.0764744 23.0557769 16.0764744 29.8698539 17.6951564 29.8698539 17.6951564 26.6812749 20.2564826 29.8698539 22.2084227 29.8698539zM25 29.8698539L25 23.0557769 23.381318 23.0557769 23.381318 29.8698539 25 29.8698539zM24.4742178 8.98009586L24.4742178 5.59616787C24.4732494 5.5136357 24.4163953 5.44228159 24.3362569 5.42252065 23.0272354 5.11977548 21.7162784 4.97854841 20.4033857 4.99883944 18.3648587 5.0303453 15.6405277 6.73541461 15.8150159 9.26543298 15.9313414 10.9521119 16.9379659 12.3146739 18.8348893 13.3531189 21.1050121 14.6587079 22.1112168 16.0505228 21.8535034 17.5285637 21.4669332 19.7456249 19.4833026 20.2699349 18.2011186 20.9636596 19.8933668 21.0568854 21.1108284 20.9541788 21.8535034 20.6555398 23.5576643 19.970275 24.621281 18.4776117 24.8765595 17.2814785 25.5814 13.9788769 23.0921699 12.4640398 21.8535034 11.6272857 20.6148368 10.7905315 18.5555838 9.39712448 18.5555838 8.2423436 18.5555838 7.08756273 19.0354769 6.19945178 20.606059 5.98878728 22.2560942 5.76746561 23.8084838 6.80552306 24.0666162 8.65926511 24.1000214 8.89915966 24.2358886 9.00610324 24.4742178 8.98009586z"/>
|
||||
<path d="M12.1896778,5.73473633 C12.2458703,5.76929923 12.2836806,5.79287044 12.3031088,5.80544997 C13.8305405,6.79444234 14.5459886,7.96859313 14.4494531,9.32790236 C14.3458984,10.7860487 13.4278718,12.1833682 11.6953731,13.5198609 C11.995423,13.6024263 13.0716006,15.2517434 14.923906,18.4678119 C15.9400176,19.5870375 17.2645126,20.0440386 18.8973912,19.8388151 C17.7166822,20.6938532 16.5941307,21.0918329 15.5297368,21.032754 C13.9331458,20.9441357 12.5153495,20.0153267 11.6953731,18.9752651 C10.8753968,17.9352035 9.17647457,14.3916396 8.02078511,13.3656207 C9.24887971,13.3176267 10.0712516,13.0717507 10.4879009,12.6279929 C11.0163711,12.0651387 11.4324817,11.1727564 11.3052905,9.86386602 C11.242381,9.21648063 10.8576813,8.46000935 10.2600254,7.66096138 C10.1677374,7.53757512 10.1984144,7.42387917 10.3520565,7.31987355 C10.8366434,7.01067102 11.3224095,6.50739801 11.8093549,5.81005452 L11.8102004,5.81066099 C11.8971472,5.68944809 12.0629706,5.65600737 12.1900999,5.73404867 Z"/>
|
||||
<path d="M5.46922112,0 C5.93751334,0 6.45488645,0.251926659 6.49405028,0.821037745 C6.52015951,1.20044514 6.40971704,1.46961432 6.16272288,1.62854529 L6.36548563,4.50160863 L9.73880697,4.59010439 C9.80515586,4.59184498 9.86880672,4.61673544 9.91873596,4.66046503 L10.8936611,5.51433494 C11.0118247,5.61782632 11.0237189,5.79751318 10.9202275,5.91567678 C10.9171724,5.91916507 10.9140324,5.92257811 10.9108103,5.92591286 L10.3160188,6.541511 C10.2118589,6.64931459 10.0419078,6.65776756 9.92756462,6.56083181 L9.4018415,6.11514401 L9.4018415,6.11514401 L7.33749093,6.11514401 C6.98986751,6.27375711 6.78712075,6.48688034 6.72925065,6.75451369 C6.67138054,7.02214704 6.66841118,9.38843602 6.72034254,13.8533806 C6.72034254,15.5011837 6.88214839,17.3116009 7.20576008,19.2846324 L5.58460752,21.9888272 L3.70958016,19.2846324 C4.08537518,17.1566151 4.27327269,15.282922 4.27327269,13.6635531 L3.43377358,12.9035744 L4.28218079,12.0252455 C4.31100967,8.71955904 4.31100967,6.96264844 4.28218079,6.75451369 C4.23893746,6.44231156 4.03865152,6.30830705 3.71848826,6.11514401 L1.69132923,6.11514401 L1.15474102,6.5615377 C1.03891569,6.65789407 0.868043682,6.64720638 0.765127903,6.53716821 L0.191996049,5.92437216 C0.0855950374,5.81060756 0.0905023165,5.63241981 0.203003442,5.52468375 L1.09677655,4.66876709 C1.14782548,4.61988037 1.2152487,4.59175365 1.28590527,4.58986886 L4.5946007,4.50160863 L4.5946007,4.50160863 L4.76223107,1.62854529 C4.55067524,1.43081789 4.44489732,1.16164871 4.44489732,0.821037745 C4.44489732,0.310121294 5.0009289,0 5.46922112,0 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.",
|
||||
"duplicateCodes": "Duplicar códigos",
|
||||
"noDuplicates": "✨ No hay duplicados",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar",
|
||||
"deduplicateCodes": "Desduplicar códigos",
|
||||
"deselectAll": "Deseleccionar todo",
|
||||
"selectAll": "Seleccionar todo",
|
||||
@@ -509,6 +510,7 @@
|
||||
"supportEnte": "Apoya a <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Danos una estrella en GitHub",
|
||||
"free5GB": "5 GB gratis en <bold-green>ente</bold-green> Fotos",
|
||||
"loginWithAuthAccount": "Inicia sesión con tu cuenta de Auth",
|
||||
"freeStorageOffer": "10% de descuento en <bold-green>ente</bold-green> fotos",
|
||||
"freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año"
|
||||
}
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.",
|
||||
"duplicateCodes": "Codici duplicati",
|
||||
"noDuplicates": "✨ Nessun doppione",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati",
|
||||
"deduplicateCodes": "Codici deduplicati",
|
||||
"deselectAll": "Deselezionare tutti",
|
||||
"selectAll": "Seleziona tutti",
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。",
|
||||
"duplicateCodes": "重複コード",
|
||||
"noDuplicates": "✨ 重複なし",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません",
|
||||
"deduplicateCodes": "重複コード",
|
||||
"deselectAll": "すべての選択を解除",
|
||||
"selectAll": "すべて選択",
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.",
|
||||
"duplicateCodes": "Dubliuoti kodus",
|
||||
"noDuplicates": "✨ Dublikatų nėra",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.",
|
||||
"deduplicateCodes": "Atdubliuoti kodus",
|
||||
"deselectAll": "Naikinti visų pasirinkimą",
|
||||
"selectAll": "Pasirinkti viską",
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"account": "അക്കൗണ്ട്",
|
||||
"unlock": "അൺലോക്ക്",
|
||||
"qrCode": "QR കോഡ്",
|
||||
"blog": "ബ്ലോഗ്",
|
||||
"verifyPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക",
|
||||
"recreatePassword": "പാസ്വേഡ് പുനഃസൃഷ്ടിക്കുക",
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
"useRecoveryKey": "Uporabi ključ za obnovo",
|
||||
"incorrectPasswordTitle": "Nepravilno geslo",
|
||||
"welcomeBack": "Dobrodošli nazaj!",
|
||||
"emailAlreadyRegistered": "E-poštni naslov je že registriran.",
|
||||
"emailNotRegistered": "E-poštni naslov ni registriran.",
|
||||
"madeWithLoveAtPrefix": "ustvarjeno s ❤️pri ",
|
||||
"supportDevs": "Naročite se na <bold-green>ente</bold-green>, da nas podprete",
|
||||
"supportDiscount": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto",
|
||||
@@ -156,6 +158,7 @@
|
||||
"twoFactorAuthTitle": "Dvojno preverjanja pristnosti",
|
||||
"passkeyAuthTitle": "Potrditev ključa za dostop (passkey)",
|
||||
"verifyPasskey": "Potrdite ključ za dostop (passkey)",
|
||||
"loginWithTOTP": "Prijava z TOTP",
|
||||
"recoverAccount": "Obnovi račun",
|
||||
"enterRecoveryKeyHint": "Vnesite vaš ključ za obnovitev",
|
||||
"recover": "Obnovi",
|
||||
@@ -257,6 +260,10 @@
|
||||
"areYouSureYouWantToLogout": "Ali ste prepričani, da se želite odjaviti?",
|
||||
"yesLogout": "Ja, odjavi se",
|
||||
"exit": "Izhod",
|
||||
"theme": "Tema",
|
||||
"lightTheme": "Svetla",
|
||||
"darkTheme": "Temna",
|
||||
"systemTheme": "Sistemska",
|
||||
"verifyingRecoveryKey": "Preverjanje ključa za obnovitev",
|
||||
"recoveryKeyVerified": "Ključ za obnovitev preverjen",
|
||||
"recoveryKeySuccessBody": "Odlično! Vaš ključ za obnovitev je veljaven. Hvala za preverjanje.\n\nNe pozabite shraniti varnostno kopijo obnovitvenega ključa.",
|
||||
@@ -327,6 +334,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Po meri",
|
||||
"editOrder": "Uredi vrstni red",
|
||||
"mostFrequentlyUsed": "Pogosto uporabljeni",
|
||||
"mostRecentlyUsed": "Nedavno uporabljeno",
|
||||
"activeSessions": "Aktivne seje",
|
||||
@@ -448,6 +457,8 @@
|
||||
"customEndpoint": "Povezano na {endpoint}",
|
||||
"pinText": "Pripni",
|
||||
"unpinText": "Odpni",
|
||||
"pinnedCodeMessage": "{code} je bila pripeta",
|
||||
"unpinnedCodeMessage": "{code} je bila odpeta",
|
||||
"pinned": "Pripeto",
|
||||
"tags": "Oznake",
|
||||
"createNewTag": "Ustvari novo oznako",
|
||||
@@ -485,5 +496,21 @@
|
||||
"appLockNotEnabled": "Zaklepanje aplikacije ni omogočeno",
|
||||
"appLockNotEnabledDescription": "Prosimo, omogočite zaklepanje aplikacije v Nastavitve > Zaklepanje Aplikacije (Security > App Lock)",
|
||||
"authToViewPasskey": "Da vidite passkey, se overite",
|
||||
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen."
|
||||
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen.",
|
||||
"duplicateCodes": "Podvojene kode",
|
||||
"noDuplicates": "✨ Ni duplikatov",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Nimate nobenih podvojenih kod, ki bi jih bilo mogoče izbrisati",
|
||||
"deduplicateCodes": "Dedupliciraj kode",
|
||||
"deselectAll": "Prekliči celoten izbor",
|
||||
"selectAll": "Izberi vse",
|
||||
"deleteDuplicates": "Izbriši dvojnike",
|
||||
"plainHTML": "Navadni HTML",
|
||||
"tellUsWhatYouThink": "Povejte nam kaj mislite",
|
||||
"dropReview": "Napišite oceno v trgovini App/Play Store",
|
||||
"supportEnte": "Podpiraj <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Dajte nam zvezdico na Githubu",
|
||||
"free5GB": "5 GB zastonj na <bold-green>ente</bold-green> fotografije",
|
||||
"loginWithAuthAccount": "Prijavite se s svojim Auth računom",
|
||||
"freeStorageOffer": "10 % popust na <bold-green>ente</bold-green> fotografije",
|
||||
"freeStorageOfferDescription": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto"
|
||||
}
|
||||
@@ -267,7 +267,9 @@
|
||||
"verifyingRecoveryKey": "Verifierar återställningsnyckel...",
|
||||
"recoveryKeyVerified": "Återställningsnyckel verifierad",
|
||||
"recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.",
|
||||
"invalidRecoveryKey": "Återställningsnyckeln du angav är inte giltig. Kontrollera att den innehåller 24 ord och kontrollera stavningen av varje ord.\n\nOm du har angett en äldre återställningskod, se till att den är 64 tecken lång, och kontrollera var och en av bokstäverna.",
|
||||
"recreatePasswordTitle": "Återskapa lösenord",
|
||||
"recreatePasswordBody": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).",
|
||||
"invalidKey": "Ogiltig nyckel",
|
||||
"tryAgain": "Försök igen",
|
||||
"viewRecoveryKey": "Visa återställningsnyckel",
|
||||
@@ -279,6 +281,10 @@
|
||||
"copyEmailAddress": "Kopiera e-postadress",
|
||||
"exportLogs": "Exportera loggar",
|
||||
"enterYourRecoveryKey": "Ange din återställningsnyckel",
|
||||
"tempErrorContactSupportIfPersists": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
|
||||
"networkHostLookUpErr": "Det gick inte att ansluta till Ente, kontrollera dina nätverksinställningar och kontakta supporten om felet kvarstår.",
|
||||
"networkConnectionRefusedErr": "Det gick inte att ansluta till Ente, försök igen om en stund. Om felet kvarstår, vänligen kontakta support.",
|
||||
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
|
||||
"about": "Om",
|
||||
"weAreOpenSource": "Vi är öppen källkod!",
|
||||
"privacy": "Sekretess",
|
||||
@@ -292,6 +298,7 @@
|
||||
"checking": "Kontrollerar ...",
|
||||
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
|
||||
"warning": "Varning",
|
||||
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
|
||||
"iUnderStand": "Jag förstår",
|
||||
"@iUnderStand": {
|
||||
"description": "Text for the button to confirm the user understands the warning"
|
||||
@@ -309,28 +316,46 @@
|
||||
}
|
||||
},
|
||||
"sorry": "Tyvärr",
|
||||
"importFailureDesc": "Det gick inte att tolka den valda filen.\nSkriv till support@ente.io om du behöver hjälp!",
|
||||
"pendingSyncs": "Varning",
|
||||
"pendingSyncsWarningBody": "En del av dina koder har inte säkerhetskopierats.\n\nSe till att du har en säkerhetskopia för dessa koder innan du loggar ut.",
|
||||
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
|
||||
"tapToEnterCode": "Tryck för att ange kod",
|
||||
"resendEmail": "Skicka e-post igen",
|
||||
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
|
||||
"@weHaveSendEmailTo": {
|
||||
"description": "Text to indicate that we have sent a mail to the user",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"description": "The email address of the user",
|
||||
"type": "String",
|
||||
"example": "example@ente.io"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Anpassad",
|
||||
"editOrder": "Redigera ordning",
|
||||
"mostFrequentlyUsed": "Ofta använd",
|
||||
"mostRecentlyUsed": "Senast använd",
|
||||
"activeSessions": "Aktiva sessioner",
|
||||
"somethingWentWrongPleaseTryAgain": "Något gick fel, vänligen försök igen",
|
||||
"thisWillLogYouOutOfThisDevice": "Detta kommer att logga ut dig från den här enheten!",
|
||||
"thisWillLogYouOutOfTheFollowingDevice": "Detta kommer att logga ut dig från följande enhet:",
|
||||
"terminateSession": "Avsluta session?",
|
||||
"terminate": "Avsluta",
|
||||
"thisDevice": "Den här enheten",
|
||||
"toResetVerifyEmail": "För att återställa ditt lösenord måste du först bekräfta din e-postadress.",
|
||||
"thisEmailIsAlreadyInUse": "Denna e-postadress används redan",
|
||||
"verificationFailedPleaseTryAgain": "Verifiering misslyckades, vänligen försök igen",
|
||||
"yourVerificationCodeHasExpired": "Din verifieringskod har upphört att gälla",
|
||||
"incorrectCode": "Felaktig kod",
|
||||
"sorryTheCodeYouveEnteredIsIncorrect": "Tyvärr, den kod som du har angett är felaktig",
|
||||
"emailChangedTo": "E-post ändrad till {newEmail}",
|
||||
"authenticationFailedPleaseTryAgain": "Autentisering misslyckades, vänligen försök igen",
|
||||
"authenticationSuccessful": "Autentisering lyckades!",
|
||||
"twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd",
|
||||
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
|
||||
"theRecoveryKeyYouEnteredIsIncorrect": "Återställningsnyckeln du angav är felaktig",
|
||||
"enterPassword": "Ange lösenord",
|
||||
"selectExportFormat": "Välj exportformat",
|
||||
"encrypted": "Krypterad",
|
||||
@@ -343,6 +368,7 @@
|
||||
"showLargeIcons": "Visa stora ikoner",
|
||||
"compactMode": "Kompakt läge",
|
||||
"shouldHideCode": "Dölj koder",
|
||||
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
|
||||
"focusOnSearchBar": "Fokusera på sök vid appstart",
|
||||
"minimizeAppOnCopy": "Minimera appen vid kopiering",
|
||||
"editCodeAuthMessage": "Autentisera för att redigera kod",
|
||||
|
||||
@@ -112,8 +112,9 @@ class Code {
|
||||
String issuer,
|
||||
String secret,
|
||||
CodeDisplay? display,
|
||||
int digits,
|
||||
) {
|
||||
int digits, {
|
||||
Algorithm algorithm = Algorithm.sha1,
|
||||
}) {
|
||||
final String encodedIssuer = Uri.encodeQueryComponent(issuer);
|
||||
return Code(
|
||||
account,
|
||||
@@ -121,10 +122,10 @@ class Code {
|
||||
digits,
|
||||
defaultPeriod,
|
||||
secret,
|
||||
Algorithm.sha1,
|
||||
algorithm,
|
||||
type,
|
||||
0,
|
||||
"otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
|
||||
"otpauth://${type.name}/$issuer:$account?algorithm=${algorithm.name.toUpperCase()}&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
|
||||
display: display ?? CodeDisplay(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:ente_auth/onboarding/view/common/field_label.dart';
|
||||
import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
|
||||
import 'package:ente_auth/store/code_display_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/algorithm_selector_widget.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/custom_icon_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_result.dart';
|
||||
@@ -38,10 +39,12 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final Logger _logger = Logger('_SetupEnterSecretKeyPageState');
|
||||
final int _notesLimit = 500;
|
||||
final int _otherTextLimit = 200;
|
||||
final int defaultDigits = 6;
|
||||
late TextEditingController _issuerController;
|
||||
late TextEditingController _accountController;
|
||||
late TextEditingController _secretController;
|
||||
late TextEditingController _notesController;
|
||||
late TextEditingController _digitsController;
|
||||
late bool _secretKeyObscured;
|
||||
late List<String> selectedTags = [...?widget.code?.display.tags];
|
||||
List<String> allTags = [];
|
||||
@@ -49,6 +52,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
bool isCustomIcon = false;
|
||||
String _customIconID = "";
|
||||
late IconType _iconSrc;
|
||||
late Algorithm _algorithm;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -65,6 +69,12 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
_notesController = TextEditingController(
|
||||
text: widget.code?.display.note,
|
||||
);
|
||||
_digitsController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? widget.code!.digits.toString()
|
||||
: defaultDigits.toString(),
|
||||
);
|
||||
|
||||
_secretKeyObscured = widget.code != null;
|
||||
_loadTags();
|
||||
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
|
||||
@@ -101,6 +111,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
? IconType.simpleIcon
|
||||
: IconType.customIcon;
|
||||
|
||||
_algorithm = widget.code == null ? Algorithm.sha1 : widget.code!.algorithm;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -121,6 +133,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
_issuerController.dispose();
|
||||
_accountController.dispose();
|
||||
_notesController.dispose();
|
||||
_digitsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -268,6 +281,79 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
widget.code == null
|
||||
? Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
collapsedShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
collapsedBackgroundColor: Colors.transparent,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
"Advanced",
|
||||
style: getEnteTextTheme(context).small,
|
||||
),
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
const FieldLabel("Digits"),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.number,
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter a number";
|
||||
}
|
||||
final intValue = int.tryParse(value);
|
||||
if (intValue == null) {
|
||||
return "Only integers are allowed";
|
||||
}
|
||||
if (intValue < 1 || intValue > 10) {
|
||||
return "OTP digits must be between 1 and 10";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
maxLines: 1,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
),
|
||||
),
|
||||
style: getEnteTextTheme(context).small,
|
||||
controller: _digitsController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
Row(
|
||||
children: [
|
||||
const FieldLabel("Algorithm"),
|
||||
AlgorithmSelectorWidget(
|
||||
currentAlgorithm: _algorithm,
|
||||
onSelected: (newAlgorithm) async {
|
||||
setState(() {
|
||||
_algorithm = newAlgorithm;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
alignment: WrapAlignment.start,
|
||||
@@ -322,12 +408,29 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
onPressed: () async {
|
||||
final digits =
|
||||
int.tryParse(_digitsController.text.trim());
|
||||
if (digits != null && (digits < 1 || digits > 10)) {
|
||||
String message = "Digits must be between 1 and 10";
|
||||
_showIncorrectDetailsDialog(
|
||||
context,
|
||||
message: message,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_accountController.text.trim().isEmpty &&
|
||||
_issuerController.text.trim().isEmpty) ||
|
||||
_secretController.text.trim().isEmpty) {
|
||||
_secretController.text.trim().isEmpty ||
|
||||
_digitsController.text.trim().isEmpty ||
|
||||
digits == null) {
|
||||
String message;
|
||||
if (_secretController.text.trim().isEmpty) {
|
||||
message = context.l10n.secretCanNotBeEmpty;
|
||||
} else if (_digitsController.text.isEmpty) {
|
||||
message = "Digits cannot be empty";
|
||||
} else if (digits == null) {
|
||||
message = "Digits is not a integer";
|
||||
} else {
|
||||
message =
|
||||
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
|
||||
@@ -358,6 +461,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final issuer = _issuerController.text.trim();
|
||||
final secret = _secretController.text.trim().replaceAll(' ', '');
|
||||
final notes = _notesController.text.trim();
|
||||
final digits = int.tryParse(_digitsController.text.trim());
|
||||
|
||||
final isStreamCode = issuer.toLowerCase() == "steam" ||
|
||||
issuer.toLowerCase().contains('steampowered.com');
|
||||
final CodeDisplay display =
|
||||
@@ -398,14 +503,18 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
issuer,
|
||||
secret,
|
||||
display,
|
||||
isStreamCode ? Code.steamDigits : Code.defaultDigits,
|
||||
isStreamCode ? Code.steamDigits : digits!,
|
||||
algorithm: _algorithm,
|
||||
)
|
||||
: widget.code!.copyWith(
|
||||
account: account,
|
||||
issuer: issuer,
|
||||
secret: secret,
|
||||
display: display,
|
||||
algorithm: _algorithm,
|
||||
digits: digits!,
|
||||
);
|
||||
|
||||
// Verify the validity of the code
|
||||
getOTP(newCode);
|
||||
Navigator.of(context).pop(newCode);
|
||||
|
||||
72
auth/lib/ui/algorithm_selector_widget.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AlgorithmSelectorWidget extends StatelessWidget {
|
||||
final Algorithm currentAlgorithm;
|
||||
final void Function(Algorithm) onSelected;
|
||||
const AlgorithmSelectorWidget({
|
||||
super.key,
|
||||
required this.currentAlgorithm,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Text algorithmOptionText(Algorithm algorithm) {
|
||||
return Text(
|
||||
algorithm.name.toUpperCase(),
|
||||
style: getEnteTextTheme(context).small,
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTapDown: (TapDownDetails details) async {
|
||||
final int? selectedValue = await showMenu<int>(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy,
|
||||
details.globalPosition.dx,
|
||||
details.globalPosition.dy + 300,
|
||||
),
|
||||
items: List.generate(Algorithm.values.length, (index) {
|
||||
return PopupMenuItem(
|
||||
value: index,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
algorithmOptionText(Algorithm.values[index]),
|
||||
if (Algorithm.values[index] == currentAlgorithm)
|
||||
Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (selectedValue != null) {
|
||||
onSelected(Algorithm.values[selectedValue]);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
algorithmOptionText(currentAlgorithm),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ jobs:
|
||||
"${{ startsWith(github.ref, 'refs/tags/v') &&
|
||||
format('photosd-{0}', github.ref_name) || ( inputs.source
|
||||
|| 'main' ) }}"
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"objectWrap": "collapse",
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## v1.7.11 (Unreleased)
|
||||
|
||||
- Improved file viewer.
|
||||
- Improved live photo experience.
|
||||
- .
|
||||
|
||||
## v1.7.10
|
||||
|
||||
@@ -10,19 +10,21 @@ To know more about Ente, see [our main README](../README.md) or visit
|
||||
|
||||
## Building from source
|
||||
|
||||
Fetch submodules
|
||||
Clone this repository and change to this directory
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
git clone https://github.com/ente-io/ente
|
||||
cd ente/desktop
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
Install dependencies (requires Yarn v1):
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
Run in development mode (supports hot reload for the renderer process)
|
||||
Now you can run in development mode (supports hot reload for the renderer
|
||||
process)
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
|
||||
@@ -27,23 +27,17 @@ export default ts.config(
|
||||
// Allow numbers to be used in template literals.
|
||||
"@typescript-eslint/restrict-template-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowNumber: true,
|
||||
},
|
||||
{ allowNumber: true },
|
||||
],
|
||||
// Allow void expressions as the entire body of an arrow function.
|
||||
"@typescript-eslint/no-confusing-void-expression": [
|
||||
"error",
|
||||
{
|
||||
ignoreArrowShorthand: true,
|
||||
},
|
||||
{ ignoreArrowShorthand: true },
|
||||
],
|
||||
// Allow free standing ternary expressions.
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
allowTernary: true,
|
||||
},
|
||||
{ allowTernary: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
"main": "app/main.js",
|
||||
"scripts": {
|
||||
"build": "yarn build-renderer && yarn build-main",
|
||||
"build:ci": "yarn build-renderer && tsc",
|
||||
"build:quick": "yarn build-renderer && yarn build-main:quick",
|
||||
"build-main": "tsc && electron-builder",
|
||||
"build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null",
|
||||
"build-renderer": "cross-env-shell _ENTE_IS_DESKTOP=1 \"cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -rf out && shx cp -r ../web/apps/photos/out out\"",
|
||||
"build:ci": "yarn build-renderer && tsc",
|
||||
"build:quick": "yarn build-renderer && yarn build-main:quick",
|
||||
"dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"",
|
||||
"dev-main": "tsc && electron .",
|
||||
"dev-renderer": "cross-env-shell _ENTE_IS_DESKTOP=1 \"cd ../web && yarn install && yarn workspace photos next dev -p 3008\"",
|
||||
@@ -31,7 +31,7 @@
|
||||
"clip-bpe-js": "^0.0.6",
|
||||
"comlink": "^4.4.2",
|
||||
"compare-versions": "^6.1.1",
|
||||
"electron-log": "^5.3.0",
|
||||
"electron-log": "^5.3.2",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.4.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
@@ -41,23 +41,22 @@
|
||||
"onnxruntime-node": "^1.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/auto-launch": "^5.0.5",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/ffmpeg-static": "^3.0.3",
|
||||
"ajv": "^8.17.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^34.1.1",
|
||||
"electron": "^34.3.1",
|
||||
"electron-builder": "^26.0.0",
|
||||
"eslint": "^9",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.8",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.23.0"
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"productName": "ente"
|
||||
|
||||
@@ -247,12 +247,7 @@ const registerPrivilegedSchemes = () => {
|
||||
corsEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
scheme: "stream",
|
||||
privileges: {
|
||||
supportFetchAPI: true,
|
||||
},
|
||||
},
|
||||
{ scheme: "stream", privileges: { supportFetchAPI: true } },
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,17 +36,9 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
|
||||
{
|
||||
label: "Ente Photos",
|
||||
submenu: [
|
||||
...macOSOnly([
|
||||
{
|
||||
label: "About Ente",
|
||||
role: "about",
|
||||
},
|
||||
]),
|
||||
...macOSOnly([{ label: "About Ente", role: "about" }]),
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Check for Updates...",
|
||||
click: handleCheckForUpdates,
|
||||
},
|
||||
{ label: "Check for Updates...", click: handleCheckForUpdates },
|
||||
{ type: "separator" },
|
||||
|
||||
...macOSOnly([
|
||||
@@ -65,20 +57,11 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
|
||||
|
||||
{ type: "separator" },
|
||||
...macOSOnly([
|
||||
{
|
||||
label: "Hide Ente",
|
||||
role: "hide",
|
||||
},
|
||||
{
|
||||
label: "Hide Others",
|
||||
role: "hideOthers",
|
||||
},
|
||||
{ label: "Hide Ente", role: "hide" },
|
||||
{ label: "Hide Others", role: "hideOthers" },
|
||||
{ type: "separator" },
|
||||
]),
|
||||
{
|
||||
label: "Quit",
|
||||
role: "quit",
|
||||
},
|
||||
{ label: "Quit", role: "quit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -96,14 +79,8 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
|
||||
{
|
||||
label: "Speech",
|
||||
submenu: [
|
||||
{
|
||||
role: "startSpeaking",
|
||||
label: "Start Speaking",
|
||||
},
|
||||
{
|
||||
role: "stopSpeaking",
|
||||
label: "Stop Speaking",
|
||||
},
|
||||
{ role: "startSpeaking", label: "Start Speaking" },
|
||||
{ role: "stopSpeaking", label: "Stop Speaking" },
|
||||
],
|
||||
},
|
||||
]),
|
||||
@@ -132,15 +109,7 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "Ente Help",
|
||||
click: handleHelp,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ label: "Help", submenu: [{ label: "Ente Help", click: handleHelp }] },
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -159,13 +128,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
|
||||
};
|
||||
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Open Ente",
|
||||
click: handleOpen,
|
||||
},
|
||||
{
|
||||
label: "Quit Ente",
|
||||
click: handleClose,
|
||||
},
|
||||
{ label: "Open Ente", click: handleOpen },
|
||||
{ label: "Quit Ente", click: handleClose },
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -10,10 +10,7 @@ class AutoLauncher {
|
||||
|
||||
constructor() {
|
||||
if (process.platform != "darwin") {
|
||||
this.autoLaunch = new AutoLaunch({
|
||||
name: "ente",
|
||||
isHidden: true,
|
||||
});
|
||||
this.autoLaunch = new AutoLaunch({ name: "ente", isHidden: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -247,9 +247,7 @@ export const computeCLIPImageEmbedding = async (
|
||||
) => {
|
||||
const session = await cachedCLIPImageSession();
|
||||
const inputArray = new Uint8Array(input.buffer);
|
||||
const feeds = {
|
||||
input: new ort.Tensor("uint8", inputArray, inputShape),
|
||||
};
|
||||
const feeds = { input: new ort.Tensor("uint8", inputArray, inputShape) };
|
||||
const t = Date.now();
|
||||
const results = await session.run(feeds);
|
||||
log.debugString(`ONNX/CLIP image embedding took ${Date.now() - t} ms`);
|
||||
@@ -292,9 +290,7 @@ export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
|
||||
const session = sessionOrSkip;
|
||||
const tokenizer = getTokenizer();
|
||||
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
|
||||
const feeds = {
|
||||
input: new ort.Tensor("int32", tokenizedText, [1, 77]),
|
||||
};
|
||||
const feeds = { input: new ort.Tensor("int32", tokenizedText, [1, 77]) };
|
||||
|
||||
const t = Date.now();
|
||||
const results = await session.run(feeds);
|
||||
@@ -316,9 +312,7 @@ export const detectFaces = async (
|
||||
) => {
|
||||
const session = await cachedFaceDetectionSession();
|
||||
const inputArray = new Uint8Array(input.buffer);
|
||||
const feeds = {
|
||||
input: new ort.Tensor("uint8", inputArray, inputShape),
|
||||
};
|
||||
const feeds = { input: new ort.Tensor("uint8", inputArray, inputShape) };
|
||||
const t = Date.now();
|
||||
const results = await session.run(feeds);
|
||||
log.debugString(`ONNX/YOLO face detection took ${Date.now() - t} ms`);
|
||||
|
||||
@@ -84,11 +84,7 @@ export const pendingUploads = async (): Promise<PendingUploads | undefined> => {
|
||||
|
||||
if (filePaths.length == 0 && zipItems.length == 0) return undefined;
|
||||
|
||||
return {
|
||||
collectionName,
|
||||
filePaths,
|
||||
zipItems,
|
||||
};
|
||||
return { collectionName, filePaths, zipItems };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,9 +5,7 @@ interface SafeStorageStore {
|
||||
}
|
||||
|
||||
const safeStorageSchema: Schema<SafeStorageStore> = {
|
||||
encryptionKey: {
|
||||
type: "string",
|
||||
},
|
||||
encryptionKey: { type: "string" },
|
||||
};
|
||||
|
||||
export const safeStorageStore = new Store({
|
||||
|
||||
@@ -22,30 +22,13 @@ export interface UploadStatusStore {
|
||||
}
|
||||
|
||||
const uploadStatusSchema: Schema<UploadStatusStore> = {
|
||||
collectionName: {
|
||||
type: "string",
|
||||
},
|
||||
filePaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
collectionName: { type: "string" },
|
||||
filePaths: { type: "array", items: { type: "string" } },
|
||||
zipItems: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
zipPaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
items: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
zipPaths: { type: "array", items: { type: "string" } },
|
||||
};
|
||||
|
||||
export const uploadStatusStore = new Store({
|
||||
|
||||
@@ -23,12 +23,7 @@ interface UserPreferences {
|
||||
* the app is not maximized (when the app was maximized when it was being
|
||||
* quit then {@link isWindowMaximized} will be set instead).
|
||||
*/
|
||||
windowBounds?: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
windowBounds?: { x: number; y: number; width: number; height: number };
|
||||
/**
|
||||
* `true` if the app's main window is maximized the last time it was closed.
|
||||
*/
|
||||
|
||||
@@ -34,10 +34,7 @@ const watchStoreSchema: Schema<WatchStore> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoredFiles: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
ignoredFiles: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -125,7 +125,12 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
const { writable, readable } = new TransformStream();
|
||||
const stream = await zip.stream(entry);
|
||||
|
||||
const nodeWritable = Writable.fromWeb(writable);
|
||||
// Silence a type error about the Promise<void> returned by the close method
|
||||
// of writable as not being assignable to Promise<undefined> which started
|
||||
// appearing after updating to TypeScript 5.8.
|
||||
//
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
||||
const nodeWritable = Writable.fromWeb(writable as any);
|
||||
stream.pipe(nodeWritable);
|
||||
|
||||
nodeWritable.on("error", (e: unknown) => {
|
||||
|
||||
@@ -177,10 +177,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
|
||||
integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
|
||||
|
||||
"@eslint/js@^9.19.0":
|
||||
version "9.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.19.0.tgz#51dbb140ed6b49d05adc0b171c41e1a8713b7789"
|
||||
integrity sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==
|
||||
"@eslint/js@^9.21.0":
|
||||
version "9.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.21.0.tgz#4303ef4e07226d87c395b8fad5278763e9c15c08"
|
||||
integrity sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==
|
||||
|
||||
"@eslint/object-schema@^2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -317,26 +317,6 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/eslint@*":
|
||||
version "9.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584"
|
||||
integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
"@types/json-schema" "*"
|
||||
|
||||
"@types/eslint__js@^8.42.3":
|
||||
version "8.42.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint__js/-/eslint__js-8.42.3.tgz#d1fa13e5c1be63a10b4e3afe992779f81c1179a0"
|
||||
integrity sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==
|
||||
dependencies:
|
||||
"@types/eslint" "*"
|
||||
|
||||
"@types/estree@*":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
"@types/ffmpeg-static@^3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.3.tgz#605358ac6304507a75c2fd5fd861534837b19e2f"
|
||||
@@ -354,11 +334,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
|
||||
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
|
||||
|
||||
"@types/json-schema@*":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/keyv@^3.1.4":
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
|
||||
@@ -417,62 +392,62 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz#7745f4e3e4a7ae5f6f73fefcd856fd6a074189b7"
|
||||
integrity sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==
|
||||
"@typescript-eslint/eslint-plugin@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e"
|
||||
integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.23.0"
|
||||
"@typescript-eslint/type-utils" "8.23.0"
|
||||
"@typescript-eslint/utils" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
"@typescript-eslint/scope-manager" "8.26.0"
|
||||
"@typescript-eslint/type-utils" "8.26.0"
|
||||
"@typescript-eslint/utils" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/parser@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.23.0.tgz#57acb3b65fce48d12b70d119436e145842a30081"
|
||||
integrity sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==
|
||||
"@typescript-eslint/parser@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9"
|
||||
integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.23.0"
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/typescript-estree" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
"@typescript-eslint/scope-manager" "8.26.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/typescript-estree" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz#ee3bb7546421ca924b9b7a8b62a77d388193ddec"
|
||||
integrity sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==
|
||||
"@typescript-eslint/scope-manager@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz#b06623fad54a3a77fadab5f652ef75ed3780b545"
|
||||
integrity sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz#271e1eecece072d92679dfda5ccfceac3faa9f76"
|
||||
integrity sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==
|
||||
"@typescript-eslint/type-utils@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz#9ee8cc98184b5f66326578de9c097edc89da6f68"
|
||||
integrity sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.23.0"
|
||||
"@typescript-eslint/utils" "8.23.0"
|
||||
"@typescript-eslint/typescript-estree" "8.26.0"
|
||||
"@typescript-eslint/utils" "8.26.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/types@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.23.0.tgz#3355f6bcc5ebab77ef6dcbbd1113ec0a683a234a"
|
||||
integrity sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==
|
||||
"@typescript-eslint/types@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.0.tgz#c4e93a8faf3a38a8d8adb007dc7834f1c89ee7bf"
|
||||
integrity sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz#f633ef08efa656e386bc44b045ffcf9537cc6924"
|
||||
integrity sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==
|
||||
"@typescript-eslint/typescript-estree@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz#128972172005a7376e34ed2ecba4e29363b0cad1"
|
||||
integrity sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -480,22 +455,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/utils@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.23.0.tgz#b269cbdc77129fd6e0e600b168b5ef740a625554"
|
||||
integrity sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==
|
||||
"@typescript-eslint/utils@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.0.tgz#845d20ed8378a5594e6445f54e53b972aee7b3e6"
|
||||
integrity sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.23.0"
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/typescript-estree" "8.23.0"
|
||||
"@typescript-eslint/scope-manager" "8.26.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/typescript-estree" "8.26.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.23.0":
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz#40405fd26a61d23f5f4c2ed0f016a47074781df8"
|
||||
integrity sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==
|
||||
"@typescript-eslint/visitor-keys@8.26.0":
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz#a4876216756c69130ea958df3b77222c2ad95290"
|
||||
integrity sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@xmldom/xmldom@^0.8.8":
|
||||
@@ -1248,10 +1223,10 @@ electron-builder@^26.0.0:
|
||||
simple-update-notifier "2.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
electron-log@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.0.tgz#503a911983db09156965595a7ee9a39f2d9d6384"
|
||||
integrity sha512-ILgbh2k9IKbSaN8NAbQriVteEhmkdLo/e4J1dg+JIBTFzXS/kO8zNRZBh/4YPwIT/zeyxF1jP6Xz8GLsPE2IBQ==
|
||||
electron-log@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.2.tgz#76aa0091f9cbf0d304546ca6f271ebb6ad953bf4"
|
||||
integrity sha512-EFI5MFFEzFJU5gyhJNpKQhfGfrRP9IWzSu0sSxrWXasWKvVAOFgBySafX8W1pbPKa/w8/DDPu2bBBtVZJdDsnw==
|
||||
|
||||
electron-publish@26.0.0:
|
||||
version "26.0.0"
|
||||
@@ -1289,10 +1264,10 @@ electron-updater@^6.4.0:
|
||||
semver "^7.6.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^34.1.1:
|
||||
version "34.1.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-34.1.1.tgz#1fc766e406401834fedb9747c4ca58671d9a1e46"
|
||||
integrity sha512-1aDYk9Gsv1/fFeClMrxWGoVMl7uCUgl1pe26BiTnLXmAoqEXCa3f3sCKFWV+cuDzUjQGAZcpkWhGYTgWUSQrLA==
|
||||
electron@^34.3.1:
|
||||
version "34.3.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-34.3.1.tgz#2c337a496d923463a2c7be7eaab191ad8220459b"
|
||||
integrity sha512-Vsgxc4FDGg7hjduKyvTP5qfNDxZHTliZIiWD1HlR5hHXx3BFjyVv3db/uEH1GaCU0KKyeNsBXRwS4WAOMaSH5g==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
@@ -2694,18 +2669,18 @@ 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.8:
|
||||
version "2.5.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.8.tgz#1b307fce044d0230ea8f3210f8a731c5cc1b288d"
|
||||
integrity sha512-BaGOF63I0IJZoudxpuQe17naV93BRtK8b3byWktkJReKEMX9CC4qdGUzThPDVO/AUhPzlqDiAXbp18U6X8wLKA==
|
||||
prettier-plugin-packagejson@^2.5.10:
|
||||
version "2.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7"
|
||||
integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==
|
||||
dependencies:
|
||||
sort-package-json "2.14.0"
|
||||
sort-package-json "2.15.1"
|
||||
synckit "0.9.2"
|
||||
|
||||
prettier@3.4.2:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
|
||||
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
|
||||
prettier@3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
|
||||
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
|
||||
|
||||
proc-log@^2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -3015,10 +2990,10 @@ sort-object-keys@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
|
||||
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
|
||||
|
||||
sort-package-json@2.14.0:
|
||||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.14.0.tgz#ba0c7420dc6edea4b0eb7e9f502fda63f57586d8"
|
||||
integrity sha512-xBRdmMjFB/KW3l51mP31dhlaiFmqkHLfWTfZAno8prb/wbDxwBPWFpxB16GZbiPbYr3wL41H8Kx22QIDWRe8WQ==
|
||||
sort-package-json@2.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e"
|
||||
integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==
|
||||
dependencies:
|
||||
detect-indent "^7.0.1"
|
||||
detect-newline "^4.0.0"
|
||||
@@ -3234,24 +3209,24 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
typescript-eslint@^8.23.0:
|
||||
version "8.23.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.23.0.tgz#796deb48f040146b68fcc8cb07db68b87219a8d2"
|
||||
integrity sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==
|
||||
typescript-eslint@^8.26.0:
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.26.0.tgz#f44cafdaa6edc99e3612b33b791eb77a56286320"
|
||||
integrity sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.23.0"
|
||||
"@typescript-eslint/parser" "8.23.0"
|
||||
"@typescript-eslint/utils" "8.23.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.26.0"
|
||||
"@typescript-eslint/parser" "8.26.0"
|
||||
"@typescript-eslint/utils" "8.26.0"
|
||||
|
||||
typescript@^5.4.3:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
|
||||
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
|
||||
|
||||
typescript@^5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
typescript@^5.8.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
|
||||
undici-types@~6.19.2:
|
||||
version "6.19.8"
|
||||
|
||||
@@ -115,4 +115,15 @@ clicking on "Your map" under "Locations" on the search screen.
|
||||
|
||||
## How to reset my password if I lost it?
|
||||
|
||||
On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password.
|
||||
On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password.
|
||||
|
||||
# iOS Album Backup and Organization in Ente
|
||||
|
||||
### How does Ente handle photos that are part of multiple iOS albums?
|
||||
When you select multiple albums for backup, Ente prioritizes uploading each photo to the album with the fewest photos. This means a photo will only be uploaded once, even if it exists in multiple albums on your device. If you create new albums on your device after the initial backup, those photos may not appear in the corresponding Ente album if they were already uploaded to a different album.
|
||||
|
||||
### Why don’t all photos from a new iOS album appear in the corresponding Ente album?
|
||||
If you create a new album on your device after the initial backup, the photos in that album may have already been uploaded to another album in Ente. To fix this, go to the "On Device" album in Ente, select all photos, and manually add them to the corresponding album in Ente.
|
||||
|
||||
### What happens if I reorganize my photos in the iOS Photos app after backing up?
|
||||
Reorganizing photos in the iOS Photos app (e.g., moving photos to new albums) won’t automatically reflect in Ente. You’ll need to manually add those photos to the corresponding albums in Ente to maintain consistency
|
||||
@@ -22,6 +22,25 @@ In brief,
|
||||
- You can invite 5 family members. So including yourself, it will be 6 people
|
||||
who can share a single subscription, paying only once.
|
||||
|
||||
## Storage Limits
|
||||
|
||||
If you're an admin of a family, you will be able to set storage limits for the
|
||||
members in your family plan.
|
||||
|
||||
In brief,
|
||||
|
||||
- For example, once you set a limit of 10GB for a member, their Storage
|
||||
quota for uploading photos will be limited to 10GB.
|
||||
|
||||
- Once the invited member accepts the Family invite, you will be able to see
|
||||
an edit icon in the Members List. Click on it to setup a family limit.
|
||||
|
||||
- If the admin has set a limit for any user, that limit value will be prefilled
|
||||
in the input box.
|
||||
|
||||
- Incase, if you want to remove any storage limit from a members account, you
|
||||
can click on the "Remove Limit" and they can upload photos without any limit.
|
||||
|
||||
## FAQ
|
||||
|
||||
- **Can you assign a storage quota for each individual member in the family
|
||||
|
||||
BIN
docs/docs/public/replication.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
@@ -54,6 +54,9 @@ The same principle applies if you're deploying to your custom domain.
|
||||
|
||||
## Replication
|
||||
|
||||

|
||||
<p align="center">Community contributed diagram of Ente's Replication Process</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As of now, Replication works only if all the 3 storage type
|
||||
> needs are fulfilled (1 Hot, 1 Cold and 1 Glacier Storage).
|
||||
|
||||
@@ -33,7 +33,6 @@ After cloning the main repository with
|
||||
git clone https://github.com/ente-io/ente.git
|
||||
# Or git clone git@github.com:ente-io/ente.git
|
||||
cd ente
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Create a `compose.yaml` file at the root of the project with the following
|
||||
|
||||
@@ -12,13 +12,12 @@ The getting started instructions mention using `yarn dev` (which is an alias of
|
||||
|
||||
>[!IMPORTANT]
|
||||
> Please note that Ente's Web App supports the Yarn version 1.22.xx or 1.22.22 specifically.
|
||||
> Make sure to install the right version or modify your yarn installation to meet the requirements.
|
||||
> Make sure to install the right version or modify your yarn installation to meet the requirements.
|
||||
> The user might end up into unknown version and dependency related errors if yarn
|
||||
> is on different version.
|
||||
|
||||
```sh
|
||||
cd ente/web
|
||||
git submodule update --init --recursive
|
||||
yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
|
||||
```
|
||||
@@ -163,7 +162,7 @@ npm install pm2@latest
|
||||
Copy the below contents to a file called `ecosystem.config.js` inside the
|
||||
`ente/web` directory.
|
||||
|
||||
```js
|
||||
```js
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
@@ -212,16 +211,16 @@ pm2 start
|
||||
pm2 logs all
|
||||
```
|
||||
|
||||
## Configure App Endpoints
|
||||
## Configure App Endpoints
|
||||
|
||||
> [!NOTE]
|
||||
> [!NOTE]
|
||||
> Previously, this was dependent on the env variables `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT`
|
||||
> and etc. Please check the below documentation to update your setup configurations
|
||||
|
||||
You can configure the web endpoints for the other apps including Accounts, Albums
|
||||
Family and Cast in your `museum.yaml` configuration file. Checkout
|
||||
Family and Cast in your `museum.yaml` configuration file. Checkout
|
||||
[`local.yaml`](https://github.com/ente-io/ente/blob/543411254b2bb55bd00a0e515dcafa12d12d3b35/server/configurations/local.yaml#L76-L89)
|
||||
to configure the endpoints. Make sure to setup up your DNS Records accordingly to the
|
||||
to configure the endpoints. Make sure to setup up your DNS Records accordingly to the
|
||||
similar URL's you set up in `museum.yaml`.
|
||||
|
||||
Next part is to configure the web server.
|
||||
|
||||
@@ -49,7 +49,6 @@ Then in a separate terminal, you can run (e.g) the web client
|
||||
|
||||
```sh
|
||||
cd ente/web
|
||||
git submodule update --init --recursive
|
||||
yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev
|
||||
```
|
||||
|
||||
@@ -437,81 +437,81 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2
|
||||
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b
|
||||
ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
|
||||
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
|
||||
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
|
||||
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
|
||||
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
|
||||
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
|
||||
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
|
||||
media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
|
||||
motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
|
||||
move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
native_video_player: 5d36066807b680e181473e6890dde643ac85380d
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: 0f9bc9adfc0b960e7f3bb5ec67e9a3d8193f3bdb
|
||||
sentry_flutter: f4a0466dc8855998ffd59378ec33507c7dc32d7b
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sentry_flutter: 64a43fb39ab4c7f67d8a4cad52b49e22439e58b7
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
|
||||
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
|
||||
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
|
||||
uni_links: f191d616c4db8750f74c72c988e79a83dd297fac
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
|
||||
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
|
||||
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
|
||||
uni_links: 103d3319e3383ed8bce559b96b1e219fbf02ba96
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||
video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
|
||||
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
|
||||
PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
unawaited(
|
||||
getDeviceName().then((name) {
|
||||
$.info("Device name: $name");
|
||||
getDeviceInfo().then((info) {
|
||||
$.info("Device Info: $info");
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:photos/models/device_collection.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/models/file_load_result.dart';
|
||||
import 'package:photos/models/upload_strategy.dart';
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import "package:photos/services/sync/import/model.dart";
|
||||
import 'package:sqflite/sqlite_api.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
||||
7
mobile/lib/events/file_caption_updated_event.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
import "package:photos/events/event.dart";
|
||||
|
||||
class FileCaptionUpdatedEvent extends Event {
|
||||
final int fileGeneratedID;
|
||||
|
||||
FileCaptionUpdatedEvent(this.fileGeneratedID);
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
|
||||
const baseRadius = 0.6;
|
||||
|
||||
class BaseLocation {
|
||||
final List<EnteFile> files;
|
||||
int? firstCreationTime;
|
||||
@@ -16,6 +20,54 @@ class BaseLocation {
|
||||
this.lastCreationTime,
|
||||
});
|
||||
|
||||
static List<BaseLocation> decodeJsonToList(
|
||||
String jsonString,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
final jsonList = jsonDecode(jsonString) as List;
|
||||
return jsonList
|
||||
.map((json) => BaseLocation.fromJson(json, filesMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
static String encodeListToJson(List<BaseLocation> baseLocations) {
|
||||
final jsonList =
|
||||
baseLocations.map((location) => location.toJson()).toList();
|
||||
return jsonEncode(jsonList);
|
||||
}
|
||||
|
||||
static BaseLocation fromJson(
|
||||
Map<String, dynamic> json,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return BaseLocation(
|
||||
(json['fileIDs'] as List).map((e) => filesMap[e]!).toList(),
|
||||
Location(
|
||||
latitude: json['location']['latitude'],
|
||||
longitude: json['location']['longitude'],
|
||||
),
|
||||
json['isCurrentBase'] as bool,
|
||||
firstCreationTime: json['firstCreationTime'] as int?,
|
||||
lastCreationTime: json['lastCreationTime'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fileIDs': files
|
||||
.where((file) => file.uploadedFileID != null)
|
||||
.map((file) => file.uploadedFileID!)
|
||||
.toList(),
|
||||
'location': {
|
||||
'latitude': location.latitude!,
|
||||
'longitude': location.longitude!,
|
||||
},
|
||||
'isCurrentBase': isCurrentBase,
|
||||
'firstCreationTime': firstCreationTime,
|
||||
'lastCreationTime': lastCreationTime,
|
||||
};
|
||||
}
|
||||
|
||||
int averageCreationTime() {
|
||||
if (firstCreationTime != null && lastCreationTime != null) {
|
||||
return (firstCreationTime! + lastCreationTime!) ~/ 2;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "dart:convert";
|
||||
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
import "package:photos/models/memories/people_memory.dart";
|
||||
import "package:photos/models/memories/smart_memory.dart";
|
||||
@@ -20,18 +22,29 @@ class MemoriesCache {
|
||||
final List<ToShowMemory> toShowMemories;
|
||||
final List<PeopleShownLog> peopleShownLogs;
|
||||
final List<TripsShownLog> tripsShownLogs;
|
||||
final List<BaseLocation> baseLocations;
|
||||
|
||||
MemoriesCache({
|
||||
required this.toShowMemories,
|
||||
required this.peopleShownLogs,
|
||||
required this.tripsShownLogs,
|
||||
required this.baseLocations,
|
||||
});
|
||||
|
||||
factory MemoriesCache.fromJson(Map<String, dynamic> json) {
|
||||
factory MemoriesCache.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return MemoriesCache(
|
||||
toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']),
|
||||
peopleShownLogs: PeopleShownLog.decodeJsonToList(json['peopleShownLogs']),
|
||||
tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']),
|
||||
baseLocations: json['baseLocations'] != null
|
||||
? BaseLocation.decodeJsonToList(
|
||||
json['baseLocations'],
|
||||
filesMap,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +53,7 @@ class MemoriesCache {
|
||||
'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories),
|
||||
'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs),
|
||||
'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs),
|
||||
'baseLocations': BaseLocation.encodeListToJson(baseLocations),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,8 +61,11 @@ class MemoriesCache {
|
||||
return jsonEncode(cache.toJson());
|
||||
}
|
||||
|
||||
static MemoriesCache decodeFromJsonString(String jsonString) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString));
|
||||
static MemoriesCache decodeFromJsonString(
|
||||
String jsonString,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,11 @@ extension SectionTypeExtensions on SectionType {
|
||||
}
|
||||
}
|
||||
|
||||
bool get sortByName => this != SectionType.face && this != SectionType.magic;
|
||||
// TODO: lau: check if we should sort moment again
|
||||
bool get sortByName =>
|
||||
this != SectionType.face &&
|
||||
this != SectionType.magic &&
|
||||
this != SectionType.moment;
|
||||
|
||||
bool get isEmptyCTAVisible {
|
||||
switch (this) {
|
||||
@@ -242,6 +246,7 @@ extension SectionTypeExtensions on SectionType {
|
||||
|
||||
case SectionType.moment:
|
||||
if (flagService.internalUser) {
|
||||
// TODO: lau: remove this whole smart memories and moment altogether
|
||||
return SearchService.instance.smartMemories(context, limit);
|
||||
}
|
||||
return SearchService.instance.getRandomMomentsSearchResults(context);
|
||||
|
||||
@@ -11,6 +11,7 @@ import "package:photos/services/machine_learning/face_ml/face_recognition_servic
|
||||
import "package:photos/services/machine_learning/machine_learning_controller.dart";
|
||||
import "package:photos/services/magic_cache_service.dart";
|
||||
import "package:photos/services/memories_cache_service.dart";
|
||||
import "package:photos/services/permission/service.dart";
|
||||
import "package:photos/services/smart_memories_service.dart";
|
||||
import "package:photos/services/storage_bonus_service.dart";
|
||||
import "package:photos/services/sync/trash_sync_service.dart";
|
||||
@@ -143,3 +144,9 @@ FaceRecognitionService get faceRecognitionService {
|
||||
_faceRecognitionService ??= FaceRecognitionService();
|
||||
return _faceRecognitionService!;
|
||||
}
|
||||
|
||||
PermissionService? _permissionService;
|
||||
PermissionService get permissionService {
|
||||
_permissionService ??= PermissionService(ServiceLocator.instance.prefs);
|
||||
return _permissionService!;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/location_tag_updated_event.dart";
|
||||
import "package:photos/extensions/stop_watch.dart";
|
||||
import "package:photos/models/api/entity/type.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/models/local_entity_data.dart";
|
||||
import "package:photos/models/location/location.dart";
|
||||
@@ -33,6 +34,8 @@ class LocationService {
|
||||
|
||||
List<City> _cities = [];
|
||||
|
||||
List<BaseLocation> baseLocations = [];
|
||||
|
||||
LocationService(this.prefs) {
|
||||
debugPrint('LocationService constructor');
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
|
||||
@@ -147,11 +147,13 @@ class SemanticSearchService {
|
||||
}
|
||||
final textEmbedding = await _getTextEmbedding(query);
|
||||
|
||||
final queryResults = await _getSimilarities(
|
||||
textEmbedding,
|
||||
minimumSimilarity: similarityThreshold,
|
||||
final similarityResults = await _getSimilarities(
|
||||
{query: textEmbedding},
|
||||
minimumSimilarityMap: {
|
||||
query: similarityThreshold ?? kMinimumSimilarityThreshold,
|
||||
},
|
||||
);
|
||||
|
||||
final queryResults = similarityResults[query]!;
|
||||
// print query for top ten scores
|
||||
for (int i = 0; i < min(10, queryResults.length); i++) {
|
||||
final result = queryResults[i];
|
||||
@@ -196,18 +198,32 @@ class SemanticSearchService {
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<int>> getMatchingFileIDs(
|
||||
String query,
|
||||
double minimumSimilarity,
|
||||
Future<Map<String, List<int>>> getMatchingFileIDs(
|
||||
Map<String, double> queryToScore,
|
||||
) async {
|
||||
final textEmbedding = await _getTextEmbedding(query);
|
||||
final textEmbeddings = <String, List<double>>{};
|
||||
final minimumSimilarityMap = <String, double>{};
|
||||
for (final entry in queryToScore.entries) {
|
||||
final query = entry.key;
|
||||
final score = entry.value;
|
||||
final textEmbedding = await _getTextEmbedding(query);
|
||||
textEmbeddings[query] = textEmbedding;
|
||||
minimumSimilarityMap[query] = score;
|
||||
}
|
||||
|
||||
final queryResults = await _getSimilarities(
|
||||
textEmbedding,
|
||||
minimumSimilarity: minimumSimilarity,
|
||||
textEmbeddings,
|
||||
minimumSimilarityMap: minimumSimilarityMap,
|
||||
);
|
||||
final result = <int>[];
|
||||
for (final r in queryResults) {
|
||||
result.add(r.id);
|
||||
final result = <String, List<int>>{};
|
||||
for (final entry in queryResults.entries) {
|
||||
final query = entry.key;
|
||||
final queryResult = entry.value;
|
||||
final fileIDs = <int>[];
|
||||
for (final result in queryResult) {
|
||||
fileIDs.add(result.id);
|
||||
}
|
||||
result[query] = fileIDs;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -249,24 +265,25 @@ class SemanticSearchService {
|
||||
return textEmbedding;
|
||||
}
|
||||
|
||||
Future<List<QueryResult>> _getSimilarities(
|
||||
List<double> textEmbedding, {
|
||||
double? minimumSimilarity,
|
||||
Future<Map<String, List<QueryResult>>> _getSimilarities(
|
||||
Map<String, List<double>> textQueryToEmbeddingMap, {
|
||||
required Map<String, double> minimumSimilarityMap,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final imageEmbeddings = await _getClipVectors();
|
||||
final List<QueryResult> queryResults = await _computer.compute(
|
||||
final Map<String, List<QueryResult>> queryResults = await _computer
|
||||
.compute<Map<String, dynamic>, Map<String, List<QueryResult>>>(
|
||||
computeBulkSimilarities,
|
||||
param: {
|
||||
"imageEmbeddings": imageEmbeddings,
|
||||
"textEmbedding": textEmbedding,
|
||||
"minimumSimilarity": minimumSimilarity,
|
||||
"textQueryToEmbeddingMap": textQueryToEmbeddingMap,
|
||||
"minimumSimilarityMap": minimumSimilarityMap,
|
||||
},
|
||||
taskName: "computeBulkSimilarities",
|
||||
);
|
||||
final endTime = DateTime.now();
|
||||
_logger.info(
|
||||
"computingSimilarities took: " +
|
||||
"computingSimilarities took for ${textQueryToEmbeddingMap.length} queries " +
|
||||
(endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch)
|
||||
.toString() +
|
||||
"ms",
|
||||
@@ -293,39 +310,44 @@ class SemanticSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
List<QueryResult> computeBulkSimilarities(Map args) {
|
||||
final queryResults = <QueryResult>[];
|
||||
Map<String, List<QueryResult>> computeBulkSimilarities(Map args) {
|
||||
final imageEmbeddings = args["imageEmbeddings"] as List<EmbeddingVector>;
|
||||
final textEmbedding = args["textEmbedding"] as List<double>;
|
||||
final minimumSimilarity = args["minimumSimilarity"] ??
|
||||
SemanticSearchService.kMinimumSimilarityThreshold;
|
||||
|
||||
final Vector textVector = Vector.fromList(textEmbedding);
|
||||
if (!kDebugMode) {
|
||||
for (final imageEmbedding in imageEmbeddings) {
|
||||
final similarity = imageEmbedding.vector.dot(textVector);
|
||||
if (similarity >= minimumSimilarity) {
|
||||
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
|
||||
final textEmbedding =
|
||||
args["textQueryToEmbeddingMap"] as Map<String, List<double>>;
|
||||
final minimumSimilarityMap =
|
||||
args["minimumSimilarityMap"] as Map<String, double>;
|
||||
final result = <String, List<QueryResult>>{};
|
||||
for (final MapEntry<String, List<double>> entry in textEmbedding.entries) {
|
||||
final query = entry.key;
|
||||
final textVector = Vector.fromList(entry.value);
|
||||
final minimumSimilarity = minimumSimilarityMap[query]!;
|
||||
final queryResults = <QueryResult>[];
|
||||
if (!kDebugMode) {
|
||||
for (final imageEmbedding in imageEmbeddings) {
|
||||
final similarity = imageEmbedding.vector.dot(textVector);
|
||||
if (similarity >= minimumSimilarity) {
|
||||
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
double bestScore = 0.0;
|
||||
for (final imageEmbedding in imageEmbeddings) {
|
||||
final similarity = imageEmbedding.vector.dot(textVector);
|
||||
if (similarity >= minimumSimilarity) {
|
||||
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
|
||||
}
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity;
|
||||
}
|
||||
}
|
||||
if (kDebugMode && queryResults.isEmpty) {
|
||||
dev.log("No results found for query with best score: $bestScore");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
double bestScore = 0.0;
|
||||
for (final imageEmbedding in imageEmbeddings) {
|
||||
final similarity = imageEmbedding.vector.dot(textVector);
|
||||
if (similarity >= minimumSimilarity) {
|
||||
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
|
||||
}
|
||||
if (similarity > bestScore) {
|
||||
bestScore = similarity;
|
||||
}
|
||||
}
|
||||
if (kDebugMode && queryResults.isEmpty) {
|
||||
dev.log("No results found for query with best score: $bestScore");
|
||||
}
|
||||
queryResults.sort((first, second) => second.score.compareTo(first.score));
|
||||
result[query] = queryResults;
|
||||
}
|
||||
|
||||
queryResults.sort((first, second) => second.score.compareTo(first.score));
|
||||
return queryResults;
|
||||
return result;
|
||||
}
|
||||
|
||||
class QueryResult {
|
||||
|
||||
@@ -393,14 +393,17 @@ class MagicCacheService {
|
||||
Future<List<MagicCache>> _nonEmptyMagicResults(
|
||||
List<Prompt> magicPromptsData,
|
||||
) async {
|
||||
final TimeLogger t = TimeLogger();
|
||||
final results = <MagicCache>[];
|
||||
final List<int> matchCount = [];
|
||||
final Map<String, double> queryToScore = {};
|
||||
for (Prompt prompt in magicPromptsData) {
|
||||
final fileUploadedIDs =
|
||||
await SemanticSearchService.instance.getMatchingFileIDs(
|
||||
prompt.query,
|
||||
prompt.minScore,
|
||||
);
|
||||
queryToScore[prompt.query] = prompt.minScore;
|
||||
}
|
||||
final clipResults =
|
||||
await SemanticSearchService.instance.getMatchingFileIDs(queryToScore);
|
||||
for (Prompt prompt in magicPromptsData) {
|
||||
final List<int> fileUploadedIDs = clipResults[prompt.query] ?? [];
|
||||
if (fileUploadedIDs.isNotEmpty) {
|
||||
results.add(
|
||||
MagicCache(prompt.title, fileUploadedIDs),
|
||||
@@ -408,7 +411,7 @@ class MagicCacheService {
|
||||
}
|
||||
matchCount.add(fileUploadedIDs.length);
|
||||
}
|
||||
_logger.info('magic result count $matchCount');
|
||||
_logger.info('magic result count $matchCount $t');
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,26 +140,27 @@ class MemoriesCacheService {
|
||||
// calculate memories for this period and for the next period
|
||||
final now = DateTime.now();
|
||||
final next = now.add(kMemoriesUpdateFrequency);
|
||||
final nowMemories =
|
||||
await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextMemories =
|
||||
final nowResult = await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextResult =
|
||||
await smartMemoriesService.calcMemories(next, newCache);
|
||||
w?.log("calculated new memories");
|
||||
for (final nowMemory in nowMemories) {
|
||||
for (final nowMemory in nowResult.memories) {
|
||||
newCache.toShowMemories
|
||||
.add(ToShowMemory.fromSmartMemory(nowMemory, now));
|
||||
}
|
||||
for (final nextMemory in nextMemories) {
|
||||
for (final nextMemory in nextResult.memories) {
|
||||
newCache.toShowMemories
|
||||
.add(ToShowMemory.fromSmartMemory(nextMemory, next));
|
||||
}
|
||||
newCache.baseLocations.addAll(nowResult.baseLocations);
|
||||
w?.log("added memories to cache");
|
||||
final file = File(await _getCachePath());
|
||||
if (!file.existsSync()) {
|
||||
file.createSync(recursive: true);
|
||||
}
|
||||
_cachedMemories =
|
||||
nowMemories.where((memory) => memory.shouldShowNow()).toList();
|
||||
nowResult.memories.where((memory) => memory.shouldShowNow()).toList();
|
||||
locationService.baseLocations = nowResult.baseLocations;
|
||||
await file.writeAsBytes(
|
||||
MemoriesCache.encodeToJsonString(newCache).codeUnits,
|
||||
);
|
||||
@@ -174,8 +175,14 @@ class MemoriesCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
/// WARNING: Use for testing only, TODO: lau: remove later
|
||||
Future<MemoriesCache> debugCacheForTesting() async {
|
||||
final oldCache = await _readCacheFromDisk();
|
||||
final MemoriesCache newCache = _processOldCache(oldCache);
|
||||
return newCache;
|
||||
}
|
||||
|
||||
MemoriesCache _processOldCache(MemoriesCache? oldCache) {
|
||||
final List<ToShowMemory> toShowMemories = [];
|
||||
final List<PeopleShownLog> peopleShownLogs = [];
|
||||
final List<TripsShownLog> tripsShownLogs = [];
|
||||
if (oldCache != null) {
|
||||
@@ -221,9 +228,10 @@ class MemoriesCacheService {
|
||||
}
|
||||
}
|
||||
return MemoriesCache(
|
||||
toShowMemories: toShowMemories,
|
||||
toShowMemories: [],
|
||||
peopleShownLogs: peopleShownLogs,
|
||||
tripsShownLogs: tripsShownLogs,
|
||||
baseLocations: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -259,6 +267,7 @@ class MemoriesCacheService {
|
||||
);
|
||||
}
|
||||
}
|
||||
locationService.baseLocations = cache.baseLocations;
|
||||
_logger.info('Processing of disk cache memories done');
|
||||
return memories;
|
||||
} catch (e, s) {
|
||||
@@ -294,8 +303,17 @@ class MemoriesCacheService {
|
||||
_logger.info("No memories cache found");
|
||||
return null;
|
||||
}
|
||||
final allFiles = Set<EnteFile>.from(
|
||||
await SearchService.instance.getAllFilesForSearch(),
|
||||
);
|
||||
final allFileIdsToFile = <int, EnteFile>{};
|
||||
for (final file in allFiles) {
|
||||
if (file.uploadedFileID != null) {
|
||||
allFileIdsToFile[file.uploadedFileID!] = file;
|
||||
}
|
||||
}
|
||||
final jsonString = file.readAsStringSync();
|
||||
return MemoriesCache.decodeFromJsonString(jsonString);
|
||||
return MemoriesCache.decodeFromJsonString(jsonString, allFileIdsToFile);
|
||||
}
|
||||
|
||||
Future<void> clearMemoriesCache() async {
|
||||
|
||||
38
mobile/lib/services/permission/service.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
|
||||
class PermissionService {
|
||||
static const kHasGrantedPermissionsKey = "has_granted_permissions";
|
||||
static const kPermissionStateKey = "permission_state";
|
||||
final SharedPreferences _prefs;
|
||||
PermissionService(this._prefs);
|
||||
Future<PermissionState> requestPhotoMangerPermissions() {
|
||||
return PhotoManager.requestPermissionExtend(
|
||||
requestOption: const PermissionRequestOption(
|
||||
androidPermission: AndroidPermission(
|
||||
type: RequestType.common,
|
||||
mediaLocation: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool hasGrantedPermissions() {
|
||||
return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
|
||||
}
|
||||
|
||||
bool hasGrantedLimitedPermissions() {
|
||||
return _prefs.getString(kPermissionStateKey) ==
|
||||
PermissionState.limited.toString();
|
||||
}
|
||||
|
||||
bool hasGrantedFullPermission() {
|
||||
return (_prefs.getString(kPermissionStateKey) ?? '') ==
|
||||
PermissionState.authorized.toString();
|
||||
}
|
||||
|
||||
Future<void> onUpdatePermission(PermissionState state) async {
|
||||
await _prefs.setBool(kHasGrantedPermissionsKey, true);
|
||||
await _prefs.setString(kPermissionStateKey, state.toString());
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart";
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/extensions/user_extension.dart";
|
||||
import "package:photos/models/api/collection/user.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import 'package:photos/models/collection/collection_items.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
@@ -1048,6 +1049,42 @@ class SearchService {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Add the found base locations from the location/memories service
|
||||
// TODO: lau: Add base location names
|
||||
if (limit == null || tagSearchResults.length < limit) {
|
||||
for (final BaseLocation base in locationService.baseLocations) {
|
||||
final a = (baseRadius * scaleFactor(base.location.latitude!)) /
|
||||
kilometersPerDegree;
|
||||
const b = baseRadius / kilometersPerDegree;
|
||||
tagSearchResults.add(
|
||||
GenericSearchResult(
|
||||
ResultType.location,
|
||||
"Base",
|
||||
base.files,
|
||||
onResultTap: (ctx) {
|
||||
showAddLocationSheet(
|
||||
ctx,
|
||||
base.location,
|
||||
name: "Base",
|
||||
radius: baseRadius,
|
||||
);
|
||||
},
|
||||
hierarchicalSearchFilter: LocationFilter(
|
||||
locationTag: LocationTag(
|
||||
name: "Base",
|
||||
radius: baseRadius,
|
||||
centerPoint: base.location,
|
||||
aSquare: a * a,
|
||||
bSquare: b * b,
|
||||
),
|
||||
occurrence: kMostRelevantFilter,
|
||||
matchedUploadedIDs: filesToUploadedFileIDs(base.files),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (limit == null || tagSearchResults.length < limit) {
|
||||
final results =
|
||||
await locationService.getFilesInCity(filesWithNoLocTag, '');
|
||||
@@ -1193,9 +1230,24 @@ class SearchService {
|
||||
BuildContext context,
|
||||
int? limit,
|
||||
) async {
|
||||
final memories = await memoriesCacheService.getMemories(limit);
|
||||
DateTime calcTime = DateTime.now();
|
||||
// await two seconds to let new page load first
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (limit == null) {
|
||||
final DateTime? pickedTime = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(1900),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (pickedTime != null) calcTime = pickedTime;
|
||||
}
|
||||
final cache = await memoriesCacheService.debugCacheForTesting();
|
||||
final memoriesResult = await smartMemoriesService
|
||||
.calcMemories(calcTime, cache, debugSurfaceAll: true);
|
||||
locationService.baseLocations = memoriesResult.baseLocations;
|
||||
final searchResults = <GenericSearchResult>[];
|
||||
for (final memory in memories) {
|
||||
for (final memory in memoriesResult.memories) {
|
||||
final files = Memory.filesFromMemories(memory.memories);
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "dart:async";
|
||||
import "dart:math" show min, max;
|
||||
|
||||
import "package:flutter/foundation.dart" show kDebugMode;
|
||||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:logging/logging.dart";
|
||||
@@ -9,6 +10,7 @@ import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/db/memories_db.dart";
|
||||
import "package:photos/db/ml/db.dart";
|
||||
import "package:photos/extensions/stop_watch.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/base_location.dart";
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
@@ -34,6 +36,13 @@ import "package:photos/services/machine_learning/ml_result.dart";
|
||||
import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart";
|
||||
import "package:photos/services/search_service.dart";
|
||||
|
||||
class MemoriesResult {
|
||||
final List<SmartMemory> memories;
|
||||
final List<BaseLocation> baseLocations;
|
||||
|
||||
MemoriesResult(this.memories, this.baseLocations);
|
||||
}
|
||||
|
||||
class SmartMemoriesService {
|
||||
final _logger = Logger("SmartMemoriesService");
|
||||
final _memoriesDB = MemoriesDB.instance;
|
||||
@@ -73,45 +82,58 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// One general method to get all memories, which calls on internal methods for each separate memory type
|
||||
Future<List<SmartMemory>> calcMemories(
|
||||
Future<MemoriesResult> calcMemories(
|
||||
DateTime now,
|
||||
MemoriesCache oldCache,
|
||||
) async {
|
||||
MemoriesCache oldCache, {
|
||||
bool debugSurfaceAll = false,
|
||||
}) async {
|
||||
try {
|
||||
_logger.finest('calcMemories called with time: $now');
|
||||
final TimeLogger t = TimeLogger(context: "calcMemories");
|
||||
_logger.finest('calcMemories called with time: $now $t');
|
||||
await init();
|
||||
final List<SmartMemory> memories = [];
|
||||
final allFiles = Set<EnteFile>.from(
|
||||
await SearchService.instance.getAllFilesForSearch(),
|
||||
);
|
||||
_seenTimes = await _memoriesDB.getSeenTimes();
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length: ${allFiles.length} $t");
|
||||
|
||||
final peopleMemories =
|
||||
await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs);
|
||||
final peopleMemories = await _getPeopleResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.peopleShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
_deductUsedMemories(allFiles, peopleMemories);
|
||||
memories.addAll(peopleMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after people: ${allFiles.length} $t");
|
||||
|
||||
// Trip memories
|
||||
final tripMemories = await _getTripsResults(allFiles, now);
|
||||
final (tripMemories, bases) = await _getTripsResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.tripsShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
_deductUsedMemories(allFiles, tripMemories);
|
||||
memories.addAll(tripMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after trips: ${allFiles.length} $t");
|
||||
|
||||
// Time memories
|
||||
final timeMemories = await _onThisDayOrWeekResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, timeMemories);
|
||||
memories.addAll(timeMemories);
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
_logger.finest("All files length after time: ${allFiles.length} $t");
|
||||
|
||||
// Filler memories
|
||||
final fillerMemories = await _getFillerResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, fillerMemories);
|
||||
memories.addAll(fillerMemories);
|
||||
return memories;
|
||||
_logger.finest("All files length after filler: ${allFiles.length} $t");
|
||||
return MemoriesResult(memories, bases);
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error calculating smart memories", e, s);
|
||||
return [];
|
||||
return MemoriesResult(<SmartMemory>[], <BaseLocation>[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +151,10 @@ class SmartMemoriesService {
|
||||
Future<List<PeopleMemory>> _getPeopleResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
List<PeopleShownLog> shownPeople,
|
||||
) async {
|
||||
List<PeopleShownLog> shownPeople, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
|
||||
final List<PeopleMemory> memoryResults = [];
|
||||
if (allFiles.isEmpty) return [];
|
||||
final allFileIdsToFile = <int, EnteFile>{};
|
||||
@@ -142,6 +166,7 @@ class SmartMemoriesService {
|
||||
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
|
||||
final windowEnd =
|
||||
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
|
||||
w?.log('allFiles setup');
|
||||
|
||||
// Get ordered list of important people (all named, from most to least files)
|
||||
final persons = await PersonService.instance.getPersons();
|
||||
@@ -169,6 +194,7 @@ class SmartMemoriesService {
|
||||
final bFaces = personIdToFaceIDs[b]!.length;
|
||||
return bFaces.compareTo(aFaces);
|
||||
});
|
||||
w?.log('orderedImportantPersonsID setup');
|
||||
|
||||
// Check if the user has assignmed "me"
|
||||
String? meID;
|
||||
@@ -179,6 +205,7 @@ class SmartMemoriesService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
w?.log('meID setup part 1');
|
||||
final bool isMeAssigned = meID != null;
|
||||
Map<int, List<Face>>? meFilesToFaces;
|
||||
if (isMeAssigned) {
|
||||
@@ -187,6 +214,7 @@ class SmartMemoriesService {
|
||||
meFileIDs,
|
||||
);
|
||||
}
|
||||
w?.log('meID setup part 2');
|
||||
|
||||
// Loop through the people and find all memories
|
||||
final Map<String, Map<PeopleMemoryType, PeopleMemory>> personToMemories =
|
||||
@@ -194,10 +222,12 @@ class SmartMemoriesService {
|
||||
for (final personID in orderedImportantPersonsID) {
|
||||
final personFileIDs = personIdToFileIDs[personID]!;
|
||||
final personName = personIdToPerson[personID]!.data.name;
|
||||
w?.log('start with new person $personName');
|
||||
final Map<int, List<Face>> personFilesToFaces =
|
||||
await MLDataDB.instance.getFacesForFileIDs(
|
||||
personFileIDs,
|
||||
);
|
||||
w?.log('personFilesToFaces setup');
|
||||
// Inside people loop, check for spotlight (Most likely every person will have a spotlight)
|
||||
final spotlightFiles = <EnteFile>[];
|
||||
for (final fileID in personFileIDs) {
|
||||
@@ -228,6 +258,7 @@ class SmartMemoriesService {
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory);
|
||||
}
|
||||
w?.log('spotlight setup');
|
||||
|
||||
// Inside people loop, check for youAndThem
|
||||
if (isMeAssigned && meID != personID) {
|
||||
@@ -258,12 +289,14 @@ class SmartMemoriesService {
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(PeopleMemoryType.youAndThem, () => youAndThemMemory);
|
||||
}
|
||||
w?.log('youAndThem setup');
|
||||
}
|
||||
|
||||
// Inside people loop, check for doingSomethingTogether
|
||||
if (isMeAssigned && meID != personID) {
|
||||
final vectors = await SemanticSearchService.instance
|
||||
.getClipVectorsForFileIDs(personFileIDs);
|
||||
w?.log('getting clip vectors for doingSomethingTogether');
|
||||
final activityFiles = <EnteFile>[];
|
||||
PeopleActivity lastActivity = PeopleActivity.values.first;
|
||||
activityLoop:
|
||||
@@ -277,6 +310,9 @@ class SmartMemoriesService {
|
||||
}
|
||||
final similarities = await MLComputer.instance
|
||||
.compareEmbeddings(vectors, activityVector);
|
||||
w?.log(
|
||||
'comparing embeddings for doingSomethingTogether and $activity',
|
||||
);
|
||||
for (final fileID in personFileIDs) {
|
||||
final similarity = similarities[fileID];
|
||||
if (similarity == null) continue;
|
||||
@@ -307,6 +343,7 @@ class SmartMemoriesService {
|
||||
() => activityMemory,
|
||||
);
|
||||
}
|
||||
w?.log('doingSomethingTogether setup');
|
||||
}
|
||||
|
||||
// Inside people loop, check for lastTimeYouSawThem
|
||||
@@ -355,16 +392,19 @@ class SmartMemoriesService {
|
||||
() => lastTimeMemory,
|
||||
);
|
||||
}
|
||||
w?.log('lastTimeYouSawThem setup');
|
||||
}
|
||||
|
||||
// // Surface everything just for debug checking
|
||||
// for (final personID in personToMemories.keys) {
|
||||
// for (final memoryType in PeopleMemoryType.values) {
|
||||
// if (personToMemories[personID]!.containsKey(memoryType)) {
|
||||
// memoryResults.add(personToMemories[personID]![memoryType]!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Surface everything just for debug checking
|
||||
if (surfaceAll) {
|
||||
for (final personID in personToMemories.keys) {
|
||||
final personMemories = personToMemories[personID]!;
|
||||
for (final memoryType in personMemories.keys) {
|
||||
memoryResults.add(personMemories[memoryType]!);
|
||||
}
|
||||
}
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
// Loop through the people and check if we should surface anything based on relevancy (bday, last met)
|
||||
personRelevancyLoop:
|
||||
@@ -432,6 +472,7 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
}
|
||||
w?.log('relevancy setup');
|
||||
|
||||
// Loop through the people (and memory types) and add based on rotation
|
||||
if (memoryResults.length >= 3) return memoryResults;
|
||||
@@ -471,24 +512,30 @@ class SmartMemoriesService {
|
||||
}
|
||||
if (added > 0) break peopleRotationLoop;
|
||||
}
|
||||
w?.log('rotation setup');
|
||||
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
Future<List<TripMemory>> _getTripsResults(
|
||||
Future<(List<TripMemory>, List<BaseLocation>)> _getTripsResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
) async {
|
||||
List<TripsShownLog> shownTrips, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
final List<TripMemory> memoryResults = [];
|
||||
final Iterable<LocalEntity<LocationTag>> locationTagEntities =
|
||||
(await locationService.getLocationTags());
|
||||
if (allFiles.isEmpty) return [];
|
||||
if (allFiles.isEmpty) return (<TripMemory>[], <BaseLocation>[]);
|
||||
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
|
||||
final windowEnd =
|
||||
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
|
||||
final currentMonth = currentTime.month;
|
||||
final cutOffTime = currentTime.subtract(const Duration(days: 365));
|
||||
|
||||
const tripRadius = 100.0;
|
||||
const overlapRadius = 10.0;
|
||||
|
||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||
for (int i = 0; i < locationTagEntities.length; i++) {
|
||||
tagToItemsMap[locationTagEntities.elementAt(i)] = [];
|
||||
@@ -496,12 +543,13 @@ class SmartMemoriesService {
|
||||
final List<(List<EnteFile>, Location)> smallRadiusClusters = [];
|
||||
final List<(List<EnteFile>, Location)> wideRadiusClusters = [];
|
||||
// Go through all files and cluster the ones not inside any location tag
|
||||
allFilesLoop:
|
||||
for (EnteFile file in allFiles) {
|
||||
if (!file.hasLocation ||
|
||||
file.uploadedFileID == null ||
|
||||
!file.isOwner ||
|
||||
file.creationTime == null) {
|
||||
continue;
|
||||
continue allFilesLoop;
|
||||
}
|
||||
// Check if the file is inside any location tag
|
||||
bool hasLocationTag = false;
|
||||
@@ -516,42 +564,41 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
// Cluster the files not inside any location tag (incremental clustering)
|
||||
if (!hasLocationTag) {
|
||||
// Small radius clustering for base locations
|
||||
bool foundSmallCluster = false;
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
0.6,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundSmallCluster = true;
|
||||
break;
|
||||
}
|
||||
if (hasLocationTag) continue allFilesLoop;
|
||||
// Small radius clustering for base locations
|
||||
bool addedToExistingSmallCluster = false;
|
||||
for (final cluster in smallRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
baseRadius,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
addedToExistingSmallCluster = true;
|
||||
break;
|
||||
}
|
||||
if (!foundSmallCluster) {
|
||||
smallRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
// Wide radius clustering for trip locations
|
||||
bool foundWideCluster = false;
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
100.0,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
foundWideCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
if (!addedToExistingSmallCluster) {
|
||||
smallRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
// Wide radius clustering for trip locations
|
||||
bool addedToExistingWideCluster = false;
|
||||
for (final cluster in wideRadiusClusters) {
|
||||
final clusterLocation = cluster.$2;
|
||||
if (isFileInsideLocationTag(
|
||||
clusterLocation,
|
||||
file.location!,
|
||||
tripRadius,
|
||||
)) {
|
||||
cluster.$1.add(file);
|
||||
addedToExistingWideCluster = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!addedToExistingWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
}
|
||||
|
||||
// Identify base locations
|
||||
@@ -577,12 +624,20 @@ class SmartMemoriesService {
|
||||
final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
creationTimes.last,
|
||||
);
|
||||
if (lastCreationTime.difference(firstCreationTime).inDays < 90) {
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (daysRange < 90) {
|
||||
continue;
|
||||
}
|
||||
// Check for a minimum average number of days photos are clicked in range
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (uniqueDays.length < daysRange * 0.1) continue;
|
||||
// Check that there isn't a huge time gap somewhere in the range
|
||||
final int gapThreshold = (daysRange * 0.6).round() * microSecondsInDay;
|
||||
int maxGap = 0;
|
||||
for (int i = 1; i < creationTimes.length; i++) {
|
||||
final gap = creationTimes[i] - creationTimes[i - 1];
|
||||
if (gap > maxGap) maxGap = gap;
|
||||
}
|
||||
if (maxGap > gapThreshold) continue;
|
||||
// Check if it's a current or old base location
|
||||
final bool isCurrent = lastCreationTime.isAfter(
|
||||
DateTime.now().subtract(
|
||||
@@ -604,7 +659,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
baseLocation.location,
|
||||
location,
|
||||
10.0,
|
||||
overlapRadius,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -614,7 +669,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
location,
|
||||
10.0,
|
||||
overlapRadius,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -753,25 +808,51 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// For now for testing let's just surface all base locations
|
||||
for (final baseLocation in baseLocations) {
|
||||
String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
final String? locationName = _tryFindLocationName(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
base: true,
|
||||
);
|
||||
if (locationName != null) {
|
||||
name =
|
||||
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
}
|
||||
memoryResults.add(
|
||||
TripMemory(
|
||||
// For now surface these on the location section TODO: lau: remove internal flag title
|
||||
if (surfaceAll) {
|
||||
for (final baseLocation in baseLocations) {
|
||||
String name =
|
||||
"Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
final String? locationName = _tryFindLocationName(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
name,
|
||||
0,
|
||||
0,
|
||||
baseLocation.location,
|
||||
),
|
||||
);
|
||||
base: true,
|
||||
);
|
||||
if (locationName != null) {
|
||||
name =
|
||||
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
|
||||
}
|
||||
memoryResults.add(
|
||||
TripMemory(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
name,
|
||||
nowInMicroseconds,
|
||||
windowEnd,
|
||||
baseLocation.location,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final trip in validTrips) {
|
||||
final year = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.averageCreationTime(),
|
||||
).year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
}
|
||||
return (memoryResults, baseLocations);
|
||||
}
|
||||
|
||||
// For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip
|
||||
@@ -844,17 +925,14 @@ class SmartMemoriesService {
|
||||
// Otherwise, if no trips happened in the current month,
|
||||
// look for the earliest upcoming trip in another month that has 3+ trips.
|
||||
else {
|
||||
// TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months
|
||||
final sortedUpcomingMonths =
|
||||
List<int>.generate(12, (i) => ((currentMonth + i) % 12) + 1);
|
||||
List<int>.generate(6, (i) => ((currentMonth + i) % 12) + 1);
|
||||
checkUpcomingMonths:
|
||||
for (final month in sortedUpcomingMonths) {
|
||||
if (tripsByMonthYear.containsKey(month)) {
|
||||
final List<TripMemory> thatMonthTrips = [];
|
||||
for (final trips in tripsByMonthYear[month]!.values) {
|
||||
for (final trip in trips) {
|
||||
thatMonthTrips.add(trip);
|
||||
}
|
||||
thatMonthTrips.addAll(trips);
|
||||
}
|
||||
if (thatMonthTrips.length >= 3) {
|
||||
// take and use the third earliest trip
|
||||
@@ -862,32 +940,46 @@ class SmartMemoriesService {
|
||||
(a, b) =>
|
||||
a.averageCreationTime().compareTo(b.averageCreationTime()),
|
||||
);
|
||||
final trip = thatMonthTrips[2];
|
||||
final year =
|
||||
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime())
|
||||
.year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
checkPotentialTrips:
|
||||
for (final trip in thatMonthTrips.sublist(2)) {
|
||||
for (final shownTrip in shownTrips) {
|
||||
final distance =
|
||||
calculateDistance(trip.location, shownTrip.location);
|
||||
final shownTripDate = DateTime.fromMicrosecondsSinceEpoch(
|
||||
shownTrip.lastTimeShown,
|
||||
);
|
||||
final shownRecently =
|
||||
currentTime.difference(shownTripDate) < kTripShowTimeout;
|
||||
if (distance < overlapRadius && shownRecently) {
|
||||
continue checkPotentialTrips;
|
||||
}
|
||||
}
|
||||
final year = DateTime.fromMicrosecondsSinceEpoch(
|
||||
trip.averageCreationTime(),
|
||||
).year;
|
||||
final String? locationName = _tryFindLocationName(trip.memories);
|
||||
String name = "Trip in $year";
|
||||
if (locationName != null) {
|
||||
name = "Trip to $locationName";
|
||||
} else if (year == currentTime.year - 1) {
|
||||
name = "Last year's trip";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return memoryResults;
|
||||
return (memoryResults, baseLocations);
|
||||
}
|
||||
|
||||
Future<List<TimeMemory>> _onThisDayOrWeekResults(
|
||||
@@ -1221,6 +1313,7 @@ class SmartMemoriesService {
|
||||
int? prefferedSize,
|
||||
}) async {
|
||||
try {
|
||||
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
|
||||
final fileCount = memories.length;
|
||||
final int targetSize = prefferedSize ?? 10;
|
||||
if (fileCount <= targetSize) return memories;
|
||||
@@ -1315,6 +1408,7 @@ class SmartMemoriesService {
|
||||
_logger.finest(
|
||||
'People memories selection done, returning ${finalSelection.length} memories',
|
||||
);
|
||||
w?.log('People memories selection done');
|
||||
return finalSelection;
|
||||
} catch (e, s) {
|
||||
_logger.severe('Error in _bestSelectionPeople', e, s);
|
||||
@@ -1354,9 +1448,9 @@ class SmartMemoriesService {
|
||||
final fileToScore = await MLComputer.instance
|
||||
.compareEmbeddings(vectors, _clipPositiveTextVector!);
|
||||
final fileIdToClip = <int, EmbeddingVector>{};
|
||||
for (final vector in vectors) {
|
||||
fileIdToClip[vector.fileID] = vector;
|
||||
}
|
||||
for (final vector in vectors) {
|
||||
fileIdToClip[vector.fileID] = vector;
|
||||
}
|
||||
|
||||
// Get face scores for each file
|
||||
final fileToFaceCount = <int, int>{};
|
||||
|
||||
120
mobile/lib/services/sync/import/diff.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
import "package:computer/computer.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/services/sync/import/model.dart";
|
||||
|
||||
class LocalDiffResult {
|
||||
// unique localPath Assets.
|
||||
final List<LocalPathAsset>? localPathAssets;
|
||||
|
||||
// set of File object created from localPathAssets
|
||||
List<EnteFile>? uniqueLocalFiles;
|
||||
|
||||
// newPathToLocalIDs represents new entries which needs to be synced to
|
||||
// the local db
|
||||
final Map<String, Set<String>>? newPathToLocalIDs;
|
||||
|
||||
final Map<String, Set<String>>? deletePathToLocalIDs;
|
||||
|
||||
LocalDiffResult({
|
||||
this.uniqueLocalFiles,
|
||||
this.localPathAssets,
|
||||
this.newPathToLocalIDs,
|
||||
this.deletePathToLocalIDs,
|
||||
});
|
||||
}
|
||||
|
||||
Future<LocalDiffResult> getDiffFromExistingImport(
|
||||
List<LocalPathAsset> assets,
|
||||
// current set of assets available on device
|
||||
Set<String> existingIDs, // localIDs of files already imported in app
|
||||
Map<String, Set<String>> pathToLocalIDs,
|
||||
) async {
|
||||
final Map<String, dynamic> args = <String, dynamic>{};
|
||||
args['assets'] = assets;
|
||||
args['existingIDs'] = existingIDs;
|
||||
args['pathToLocalIDs'] = pathToLocalIDs;
|
||||
final LocalDiffResult diffResult = await Computer.shared().compute(
|
||||
_getLocalAssetsDiff,
|
||||
param: args,
|
||||
taskName: "getLocalAssetsDiff",
|
||||
);
|
||||
if (diffResult.localPathAssets != null) {
|
||||
diffResult.uniqueLocalFiles =
|
||||
await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets!);
|
||||
}
|
||||
return diffResult;
|
||||
}
|
||||
|
||||
Future<List<EnteFile>> _convertLocalAssetsToUniqueFiles(
|
||||
List<LocalPathAsset> assets,
|
||||
) async {
|
||||
final Set<String> alreadySeenLocalIDs = <String>{};
|
||||
final List<EnteFile> files = [];
|
||||
for (LocalPathAsset localPathAsset in assets) {
|
||||
final String localPathName = localPathAsset.pathName;
|
||||
for (final String localID in localPathAsset.localIDs) {
|
||||
if (!alreadySeenLocalIDs.contains(localID)) {
|
||||
final assetEntity = await AssetEntity.fromId(localID);
|
||||
if (assetEntity == null) {
|
||||
Logger("_convertLocalAssetsToUniqueFiles")
|
||||
.warning('Failed to fetch asset with id $localID');
|
||||
continue;
|
||||
}
|
||||
files.add(
|
||||
await EnteFile.fromAsset(localPathName, assetEntity),
|
||||
);
|
||||
alreadySeenLocalIDs.add(localID);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// _getLocalAssetsDiff compares local db with the file system and compute
|
||||
// the files which needs to be added or removed from device collection.
|
||||
LocalDiffResult _getLocalAssetsDiff(Map<String, dynamic> args) {
|
||||
final List<LocalPathAsset> onDeviceLocalPathAsset = args['assets'];
|
||||
final Set<String> existingIDs = args['existingIDs'];
|
||||
final Map<String, Set<String>> pathToLocalIDs = args['pathToLocalIDs'];
|
||||
final Map<String, Set<String>> newPathToLocalIDs = <String, Set<String>>{};
|
||||
final Map<String, Set<String>> removedPathToLocalIDs =
|
||||
<String, Set<String>>{};
|
||||
final List<LocalPathAsset> unsyncedAssets = [];
|
||||
|
||||
for (final localPathAsset in onDeviceLocalPathAsset) {
|
||||
final String pathID = localPathAsset.pathID;
|
||||
// Start identifying pathID to localID mapping changes which needs to be
|
||||
// synced
|
||||
final Set<String> candidateLocalIDsForRemoval =
|
||||
pathToLocalIDs[pathID] ?? <String>{};
|
||||
final Set<String> missingLocalIDsInPath = <String>{};
|
||||
for (final String localID in localPathAsset.localIDs) {
|
||||
if (candidateLocalIDsForRemoval.contains(localID)) {
|
||||
// remove the localID after checking. Any pending existing ID indicates
|
||||
// the the local file was removed from the path.
|
||||
candidateLocalIDsForRemoval.remove(localID);
|
||||
} else {
|
||||
missingLocalIDsInPath.add(localID);
|
||||
}
|
||||
}
|
||||
if (candidateLocalIDsForRemoval.isNotEmpty) {
|
||||
removedPathToLocalIDs[pathID] = candidateLocalIDsForRemoval;
|
||||
}
|
||||
if (missingLocalIDsInPath.isNotEmpty) {
|
||||
newPathToLocalIDs[pathID] = missingLocalIDsInPath;
|
||||
}
|
||||
// End
|
||||
|
||||
localPathAsset.localIDs.removeAll(existingIDs);
|
||||
if (localPathAsset.localIDs.isNotEmpty) {
|
||||
unsyncedAssets.add(localPathAsset);
|
||||
}
|
||||
}
|
||||
return LocalDiffResult(
|
||||
localPathAssets: unsyncedAssets,
|
||||
newPathToLocalIDs: newPathToLocalIDs,
|
||||
deletePathToLocalIDs: removedPathToLocalIDs,
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/local_import_progress.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/services/sync/import/model.dart";
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
final _logger = Logger("FileSyncUtil");
|
||||
@@ -127,99 +128,6 @@ Future<List<LocalPathAsset>> getAllLocalAssets() async {
|
||||
return localPathAssets;
|
||||
}
|
||||
|
||||
Future<LocalDiffResult> getDiffWithLocal(
|
||||
List<LocalPathAsset> assets,
|
||||
// current set of assets available on device
|
||||
Set<String> existingIDs, // localIDs of files already imported in app
|
||||
Map<String, Set<String>> pathToLocalIDs,
|
||||
) async {
|
||||
final Map<String, dynamic> args = <String, dynamic>{};
|
||||
args['assets'] = assets;
|
||||
args['existingIDs'] = existingIDs;
|
||||
args['pathToLocalIDs'] = pathToLocalIDs;
|
||||
final LocalDiffResult diffResult = await Computer.shared().compute(
|
||||
_getLocalAssetsDiff,
|
||||
param: args,
|
||||
taskName: "getLocalAssetsDiff",
|
||||
);
|
||||
if (diffResult.localPathAssets != null) {
|
||||
diffResult.uniqueLocalFiles =
|
||||
await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets!);
|
||||
}
|
||||
return diffResult;
|
||||
}
|
||||
|
||||
// _getLocalAssetsDiff compares local db with the file system and compute
|
||||
// the files which needs to be added or removed from device collection.
|
||||
LocalDiffResult _getLocalAssetsDiff(Map<String, dynamic> args) {
|
||||
final List<LocalPathAsset> onDeviceLocalPathAsset = args['assets'];
|
||||
final Set<String> existingIDs = args['existingIDs'];
|
||||
final Map<String, Set<String>> pathToLocalIDs = args['pathToLocalIDs'];
|
||||
final Map<String, Set<String>> newPathToLocalIDs = <String, Set<String>>{};
|
||||
final Map<String, Set<String>> removedPathToLocalIDs =
|
||||
<String, Set<String>>{};
|
||||
final List<LocalPathAsset> unsyncedAssets = [];
|
||||
|
||||
for (final localPathAsset in onDeviceLocalPathAsset) {
|
||||
final String pathID = localPathAsset.pathID;
|
||||
// Start identifying pathID to localID mapping changes which needs to be
|
||||
// synced
|
||||
final Set<String> candidateLocalIDsForRemoval =
|
||||
pathToLocalIDs[pathID] ?? <String>{};
|
||||
final Set<String> missingLocalIDsInPath = <String>{};
|
||||
for (final String localID in localPathAsset.localIDs) {
|
||||
if (candidateLocalIDsForRemoval.contains(localID)) {
|
||||
// remove the localID after checking. Any pending existing ID indicates
|
||||
// the the local file was removed from the path.
|
||||
candidateLocalIDsForRemoval.remove(localID);
|
||||
} else {
|
||||
missingLocalIDsInPath.add(localID);
|
||||
}
|
||||
}
|
||||
if (candidateLocalIDsForRemoval.isNotEmpty) {
|
||||
removedPathToLocalIDs[pathID] = candidateLocalIDsForRemoval;
|
||||
}
|
||||
if (missingLocalIDsInPath.isNotEmpty) {
|
||||
newPathToLocalIDs[pathID] = missingLocalIDsInPath;
|
||||
}
|
||||
// End
|
||||
|
||||
localPathAsset.localIDs.removeAll(existingIDs);
|
||||
if (localPathAsset.localIDs.isNotEmpty) {
|
||||
unsyncedAssets.add(localPathAsset);
|
||||
}
|
||||
}
|
||||
return LocalDiffResult(
|
||||
localPathAssets: unsyncedAssets,
|
||||
newPathToLocalIDs: newPathToLocalIDs,
|
||||
deletePathToLocalIDs: removedPathToLocalIDs,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<EnteFile>> _convertLocalAssetsToUniqueFiles(
|
||||
List<LocalPathAsset> assets,
|
||||
) async {
|
||||
final Set<String> alreadySeenLocalIDs = <String>{};
|
||||
final List<EnteFile> files = [];
|
||||
for (LocalPathAsset localPathAsset in assets) {
|
||||
final String localPathName = localPathAsset.pathName;
|
||||
for (final String localID in localPathAsset.localIDs) {
|
||||
if (!alreadySeenLocalIDs.contains(localID)) {
|
||||
final assetEntity = await AssetEntity.fromId(localID);
|
||||
if (assetEntity == null) {
|
||||
_logger.warning('Failed to fetch asset with id $localID');
|
||||
continue;
|
||||
}
|
||||
files.add(
|
||||
await EnteFile.fromAsset(localPathName, assetEntity),
|
||||
);
|
||||
alreadySeenLocalIDs.add(localID);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/// returns a list of AssetPathEntity with relevant filter operations.
|
||||
/// [needTitle] impacts the performance for fetching the actual [AssetEntity]
|
||||
/// in iOS. Same is true for [containsModifiedPath]
|
||||
@@ -314,36 +222,3 @@ Future<Tuple2<Set<String>, List<EnteFile>>> _getLocalIDsAndFilesFromAssets(
|
||||
}
|
||||
return Tuple2(localIDs, files);
|
||||
}
|
||||
|
||||
class LocalPathAsset {
|
||||
final Set<String> localIDs;
|
||||
final String pathID;
|
||||
final String pathName;
|
||||
|
||||
LocalPathAsset({
|
||||
required this.localIDs,
|
||||
required this.pathName,
|
||||
required this.pathID,
|
||||
});
|
||||
}
|
||||
|
||||
class LocalDiffResult {
|
||||
// unique localPath Assets.
|
||||
final List<LocalPathAsset>? localPathAssets;
|
||||
|
||||
// set of File object created from localPathAssets
|
||||
List<EnteFile>? uniqueLocalFiles;
|
||||
|
||||
// newPathToLocalIDs represents new entries which needs to be synced to
|
||||
// the local db
|
||||
final Map<String, Set<String>>? newPathToLocalIDs;
|
||||
|
||||
final Map<String, Set<String>>? deletePathToLocalIDs;
|
||||
|
||||
LocalDiffResult({
|
||||
this.uniqueLocalFiles,
|
||||
this.localPathAssets,
|
||||
this.newPathToLocalIDs,
|
||||
this.deletePathToLocalIDs,
|
||||
});
|
||||
}
|
||||
11
mobile/lib/services/sync/import/model.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class LocalPathAsset {
|
||||
final Set<String> localIDs;
|
||||
final String pathID;
|
||||
final String pathName;
|
||||
|
||||
LocalPathAsset({
|
||||
required this.localIDs,
|
||||
required this.pathName,
|
||||
required this.pathID,
|
||||
});
|
||||
}
|
||||
@@ -13,14 +13,17 @@ import 'package:photos/db/file_updation_db.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import 'package:photos/events/backup_folders_updated_event.dart';
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/events/permission_granted_event.dart";
|
||||
import 'package:photos/events/sync_status_update_event.dart';
|
||||
import 'package:photos/extensions/stop_watch.dart';
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/ignored_file.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/app_lifecycle_service.dart';
|
||||
import "package:photos/services/ignored_files_service.dart";
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import "package:photos/services/sync/import/diff.dart";
|
||||
import "package:photos/services/sync/import/local_assets.dart";
|
||||
import "package:photos/services/sync/import/model.dart";
|
||||
import "package:photos/utils/standalone/debouncer.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
@@ -36,8 +39,6 @@ class LocalSyncService {
|
||||
|
||||
static const kDbUpdationTimeKey = "db_updation_time";
|
||||
static const kHasCompletedFirstImportKey = "has_completed_firstImport";
|
||||
static const kHasGrantedPermissionsKey = "has_granted_permissions";
|
||||
static const kPermissionStateKey = "permission_state";
|
||||
|
||||
LocalSyncService._privateConstructor();
|
||||
|
||||
@@ -49,18 +50,23 @@ class LocalSyncService {
|
||||
if (!AppLifecycleService.instance.isForeground) {
|
||||
await PhotoManager.setIgnorePermissionCheck(true);
|
||||
}
|
||||
if (hasGrantedPermissions()) {
|
||||
if (permissionService.hasGrantedPermissions()) {
|
||||
_registerChangeCallback();
|
||||
} else {
|
||||
Bus.instance.on<PermissionGrantedEvent>().listen((event) async {
|
||||
_registerChangeCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sync() async {
|
||||
if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
|
||||
if (!permissionService.hasGrantedPermissions()) {
|
||||
_logger.info("Skipping local sync since permission has not been granted");
|
||||
return;
|
||||
}
|
||||
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
|
||||
final permissionState = await requestPhotoMangerPermissions();
|
||||
final permissionState =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
if (permissionState != PermissionState.authorized) {
|
||||
_logger.severe(
|
||||
"sync requested with invalid permission",
|
||||
@@ -168,7 +174,7 @@ class LocalSyncService {
|
||||
final Map<String, Set<String>> pathToLocalIDs =
|
||||
await _db.getDevicePathIDToLocalIDMap();
|
||||
|
||||
final localDiffResult = await getDiffWithLocal(
|
||||
final localDiffResult = await getDiffFromExistingImport(
|
||||
localAssets,
|
||||
existingLocalFileIDs,
|
||||
pathToLocalIDs,
|
||||
@@ -237,36 +243,6 @@ class LocalSyncService {
|
||||
return _lock;
|
||||
}
|
||||
|
||||
bool hasGrantedPermissions() {
|
||||
return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
|
||||
}
|
||||
|
||||
bool hasGrantedLimitedPermissions() {
|
||||
return _prefs.getString(kPermissionStateKey) ==
|
||||
PermissionState.limited.toString();
|
||||
}
|
||||
|
||||
bool hasGrantedFullPermission() {
|
||||
return (_prefs.getString(kPermissionStateKey) ?? '') ==
|
||||
PermissionState.authorized.toString();
|
||||
}
|
||||
|
||||
Future<void> onUpdatePermission(PermissionState state) async {
|
||||
await _prefs.setBool(kHasGrantedPermissionsKey, true);
|
||||
await _prefs.setString(kPermissionStateKey, state.toString());
|
||||
}
|
||||
|
||||
Future<void> onPermissionGranted(PermissionState state) async {
|
||||
await _prefs.setBool(kHasGrantedPermissionsKey, true);
|
||||
await _prefs.setString(kPermissionStateKey, state.toString());
|
||||
if (state == PermissionState.limited) {
|
||||
// when limited permission is granted, by default mark all folders for
|
||||
// backup
|
||||
await Configuration.instance.setSelectAllFoldersForBackup(true);
|
||||
}
|
||||
_registerChangeCallback();
|
||||
}
|
||||
|
||||
bool hasCompletedFirstImport() {
|
||||
return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
|
||||
}
|
||||
@@ -365,7 +341,7 @@ class LocalSyncService {
|
||||
if (_existingSync != null) {
|
||||
await _existingSync!.future;
|
||||
}
|
||||
if (hasGrantedLimitedPermissions()) {
|
||||
if (permissionService.hasGrantedLimitedPermissions()) {
|
||||
unawaited(syncAll());
|
||||
} else {
|
||||
unawaited(sync().then((value) => _refreshDeviceFolderCountAndCover()));
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/permission_granted_event.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import 'package:photos/events/sync_status_update_event.dart';
|
||||
import 'package:photos/events/trigger_logout_event.dart';
|
||||
@@ -169,10 +168,7 @@ class SyncService {
|
||||
return _lastSyncStatusEvent;
|
||||
}
|
||||
|
||||
Future<void> onPermissionGranted(PermissionState state) async {
|
||||
_logger.info("Permission granted " + state.toString());
|
||||
await _localSyncService.onPermissionGranted(state);
|
||||
Bus.instance.fire(PermissionGrantedEvent());
|
||||
Future<void> onPermissionGranted() async {
|
||||
_doSync().ignore();
|
||||
}
|
||||
|
||||
|
||||
37
mobile/lib/states/detail_page_state.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
class InheritedDetailPageState extends InheritedWidget {
|
||||
final enableFullScreenNotifier = ValueNotifier(false);
|
||||
InheritedDetailPageState({
|
||||
super.key,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static InheritedDetailPageState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<InheritedDetailPageState>()!;
|
||||
|
||||
void toggleFullScreen({bool? shouldEnable}) {
|
||||
if (shouldEnable != null) {
|
||||
if (enableFullScreenNotifier.value == shouldEnable) return;
|
||||
}
|
||||
enableFullScreenNotifier.value = !enableFullScreenNotifier.value;
|
||||
if (enableFullScreenNotifier.value) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedDetailPageState oldWidget) =>
|
||||
oldWidget.enableFullScreenNotifier != enableFullScreenNotifier;
|
||||
}
|
||||
@@ -5,13 +5,11 @@ import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/services/sync/local_sync_service.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
|
||||
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/navigation_util.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
|
||||
class HomeHeaderWidget extends StatefulWidget {
|
||||
final Widget centerWidget;
|
||||
const HomeHeaderWidget({required this.centerWidget, super.key});
|
||||
@@ -48,20 +46,20 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
|
||||
onTap: () async {
|
||||
try {
|
||||
final PermissionState state =
|
||||
await requestPhotoMangerPermissions();
|
||||
await LocalSyncService.instance.onUpdatePermission(state);
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
await permissionService.onUpdatePermission(state);
|
||||
} on Exception catch (e) {
|
||||
Logger("HomeHeaderWidget").severe(
|
||||
"Failed to request permission: ${e.toString()}",
|
||||
e,
|
||||
);
|
||||
}
|
||||
if (!LocalSyncService.instance.hasGrantedFullPermission()) {
|
||||
if (!permissionService.hasGrantedFullPermission()) {
|
||||
if (Platform.isAndroid) {
|
||||
await PhotoManager.openSetting();
|
||||
} else {
|
||||
final bool hasGrantedLimit =
|
||||
LocalSyncService.instance.hasGrantedLimitedPermissions();
|
||||
permissionService.hasGrantedLimitedPermissions();
|
||||
// ignore: unawaited_futures
|
||||
showChoiceActionSheet(
|
||||
context,
|
||||
|
||||
@@ -4,12 +4,15 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import "package:photos/core/configuration.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/permission_granted_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/sync/sync_service.dart';
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import "package:styled_text/styled_text.dart";
|
||||
|
||||
class GrantPermissionsWidget extends StatefulWidget {
|
||||
@@ -106,11 +109,12 @@ class _GrantPermissionsWidgetState extends State<GrantPermissionsWidget> {
|
||||
child: Text(S.of(context).grantPermission),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final state = await requestPhotoMangerPermissions();
|
||||
final state =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
_logger.info("Permission state: $state");
|
||||
if (state == PermissionState.authorized ||
|
||||
state == PermissionState.limited) {
|
||||
await SyncService.instance.onPermissionGranted(state);
|
||||
await onPermissionGranted(state);
|
||||
} else if (state == PermissionState.denied) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
@@ -139,4 +143,16 @@ class _GrantPermissionsWidgetState extends State<GrantPermissionsWidget> {
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onPermissionGranted(PermissionState state) async {
|
||||
_logger.info("Permission granted " + state.toString());
|
||||
await permissionService.onUpdatePermission(state);
|
||||
if (state == PermissionState.limited) {
|
||||
// when limited permission is granted, by default mark all folders for
|
||||
// backup
|
||||
await Configuration.instance.setSelectAllFoldersForBackup(true);
|
||||
}
|
||||
SyncService.instance.onPermissionGranted().ignore();
|
||||
Bus.instance.fire(PermissionGrantedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:photos/ente_theme_data.dart';
|
||||
import 'package:photos/events/local_import_progress.dart';
|
||||
import 'package:photos/events/sync_status_update_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/sync/local_sync_service.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/ui/common/bottom_shadow.dart';
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/dialog_widget.dart";
|
||||
@@ -44,7 +44,7 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
|
||||
_firstImportEvent =
|
||||
Bus.instance.on<SyncStatusUpdate>().listen((event) async {
|
||||
if (mounted && event.status == SyncStatus.completedFirstGalleryImport) {
|
||||
if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
||||
if (permissionService.hasGrantedLimitedPermissions()) {
|
||||
// Do nothing, let HomeWidget refresh
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
|
||||
@@ -3,7 +3,7 @@ import "dart:async";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:photos/generated/l10n.dart';
|
||||
import 'package:photos/services/sync/local_sync_service.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/ui/common/gradient_button.dart';
|
||||
import 'package:photos/ui/settings/backup/backup_folder_selection_page.dart';
|
||||
import 'package:photos/utils/navigation_util.dart';
|
||||
@@ -42,8 +42,7 @@ class StartBackupHookWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: GradientButton(
|
||||
onTap: () async {
|
||||
if (LocalSyncService.instance
|
||||
.hasGrantedLimitedPermissions()) {
|
||||
if (permissionService.hasGrantedLimitedPermissions()) {
|
||||
unawaited(PhotoManager.presentLimited());
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
|
||||
@@ -176,7 +176,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
// Loading page will redirect to BackupFolderSelectionPage.
|
||||
// To avoid showing folder hook in middle during routing,
|
||||
// delay state refresh for home page
|
||||
if (!LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
||||
if (!permissionService.hasGrantedLimitedPermissions()) {
|
||||
delayInRefresh = const Duration(milliseconds: 250);
|
||||
}
|
||||
Future.delayed(
|
||||
@@ -643,7 +643,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
_closeDrawerIfOpen(context);
|
||||
return const LandingPageWidget();
|
||||
}
|
||||
if (!LocalSyncService.instance.hasGrantedPermissions()) {
|
||||
if (!permissionService.hasGrantedPermissions()) {
|
||||
entityService.syncEntities().then((_) {
|
||||
PersonService.instance.resetEmailToPartialPersonDataCache();
|
||||
});
|
||||
@@ -671,7 +671,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
|
||||
_showShowBackupHook =
|
||||
!Configuration.instance.hasSelectedAnyBackupFolder() &&
|
||||
!LocalSyncService.instance.hasGrantedLimitedPermissions() &&
|
||||
!permissionService.hasGrantedLimitedPermissions() &&
|
||||
CollectionsService.instance.getActiveCollections().isEmpty;
|
||||
|
||||
return Stack(
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/models/file/file_type.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/local_authentication_service.dart";
|
||||
import "package:photos/states/detail_page_state.dart";
|
||||
import "package:photos/ui/common/fast_scroll_physics.dart";
|
||||
import 'package:photos/ui/notification/toast.dart';
|
||||
import 'package:photos/ui/tools/editor/image_editor_page.dart';
|
||||
@@ -77,7 +78,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
List<EnteFile>? _files;
|
||||
late PageController _pageController;
|
||||
final _selectedIndexNotifier = ValueNotifier(0);
|
||||
final _enableFullScreenNotifier = ValueNotifier(false);
|
||||
bool _isFirstOpened = true;
|
||||
bool isGuestView = false;
|
||||
bool swipeLocked = false;
|
||||
@@ -103,7 +103,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
void dispose() {
|
||||
_guestViewEventSubscription.cancel();
|
||||
_pageController.dispose();
|
||||
_enableFullScreenNotifier.dispose();
|
||||
_selectedIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -137,96 +136,102 @@ class _DetailPageState extends State<DetailPage> {
|
||||
_files!.length.toString() +
|
||||
" files .",
|
||||
);
|
||||
return PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
return InheritedDetailPageState(
|
||||
child: PopScope(
|
||||
canPop: !isGuestView,
|
||||
onPopInvoked: (didPop) async {
|
||||
if (isGuestView) {
|
||||
final authenticated = await _requestAuthentication();
|
||||
if (authenticated) {
|
||||
Bus.instance.fire(GuestViewEvent(false, false));
|
||||
await localSettings.setOnGuestView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: _enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(80),
|
||||
child: ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileAppBar(
|
||||
_files![selectedIndex],
|
||||
_onFileRemoved,
|
||||
widget.config.mode == DetailPageMode.full,
|
||||
enableFullScreenNotifier: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
),
|
||||
),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(context),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onEditFileRequested,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier: _enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _enableFullScreenNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildPageView(),
|
||||
ValueListenableBuilder(
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
return FileBottomBar(
|
||||
_files![selectedIndex],
|
||||
_onEditFileRequested,
|
||||
widget.config.mode == DetailPageMode.minimalistic &&
|
||||
!isGuestView,
|
||||
onFileRemoved: _onFileRemoved,
|
||||
userID: Configuration.instance.getUserID(),
|
||||
enableFullScreenNotifier:
|
||||
InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
);
|
||||
},
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: _selectedIndexNotifier,
|
||||
builder: (BuildContext context, int selectedIndex, _) {
|
||||
if (_files![selectedIndex].isPanorama() == true) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
builder: (context, value, child) {
|
||||
return IgnorePointer(
|
||||
ignoring: value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: !value ? 1.0 : 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Tooltip(
|
||||
message: S.of(context).panorama,
|
||||
child: IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: const Color(0xAA252525),
|
||||
fixedSize: const Size(44, 44),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -251,7 +256,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
).ignore();
|
||||
}
|
||||
|
||||
Widget _buildPageView(BuildContext context) {
|
||||
Widget _buildPageView() {
|
||||
return PageView.builder(
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -271,14 +276,17 @@ class _DetailPageState extends State<DetailPage> {
|
||||
},
|
||||
playbackCallback: (isPlaying) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
_toggleFullScreen(shouldEnable: isPlaying);
|
||||
InheritedDetailPageState.of(context)
|
||||
.toggleFullScreen(shouldEnable: isPlaying);
|
||||
});
|
||||
},
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
file.fileType != FileType.video ? _toggleFullScreen() : null;
|
||||
file.fileType != FileType.video
|
||||
? InheritedDetailPageState.of(context).toggleFullScreen()
|
||||
: null;
|
||||
},
|
||||
child: fileContent,
|
||||
);
|
||||
@@ -313,26 +321,6 @@ class _DetailPageState extends State<DetailPage> {
|
||||
return false;
|
||||
}
|
||||
|
||||
void _toggleFullScreen({bool? shouldEnable}) {
|
||||
if (shouldEnable != null) {
|
||||
if (_enableFullScreenNotifier.value == shouldEnable) return;
|
||||
}
|
||||
_enableFullScreenNotifier.value = !_enableFullScreenNotifier.value;
|
||||
if (_enableFullScreenNotifier.value) {
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.manual,
|
||||
overlays: [],
|
||||
);
|
||||
});
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: SystemUiOverlay.values,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _preloadFiles(int index) {
|
||||
if (index > 0) {
|
||||
preloadFile(_files![index - 1]);
|
||||
|
||||
@@ -100,11 +100,6 @@ class FileBottomBarState extends State<FileBottomBar> {
|
||||
),
|
||||
onPressed: () async {
|
||||
await _displayDetails(widget.file);
|
||||
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
|
||||
safeRefresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart';
|
||||
import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
|
||||
import "package:photos/ui/notification/toast.dart";
|
||||
import 'package:photos/utils/magic_util.dart';
|
||||
|
||||
class FileCaptionReadyOnly extends StatelessWidget {
|
||||
@@ -71,18 +74,19 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.addListener(_focusNodeListener);
|
||||
editedCaption = widget.file.caption;
|
||||
if (editedCaption != null && editedCaption!.isNotEmpty) {
|
||||
hintText = editedCaption!;
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (editedCaption != null) {
|
||||
editFileCaption(null, widget.file, editedCaption!);
|
||||
editFileCaption(null, widget.file, editedCaption!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
}
|
||||
_textController.dispose();
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
@@ -148,7 +152,8 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
Future<void> _onDoneClick(BuildContext context) async {
|
||||
if (editedCaption != null) {
|
||||
final isSuccesful =
|
||||
await editFileCaption(context, widget.file, editedCaption!);
|
||||
await editFileCaption(context, widget.file, editedCaption!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
if (isSuccesful) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
@@ -185,4 +190,15 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
KeyboardOverlay.removeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
bool _onEditFileFinish(bool isSuccess) {
|
||||
if (isSuccess) {
|
||||
widget.file.pubMagicMetadata?.caption = editedCaption;
|
||||
Bus.instance.fire(FileCaptionUpdatedEvent(widget.file.generatedID!));
|
||||
return true;
|
||||
} else {
|
||||
showShortToast(context, S.of(context).somethingWentWrong);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import "package:media_kit_video/media_kit_video.dart";
|
||||
import "package:photos/models/file/file.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/actions/file/file_actions.dart";
|
||||
import "package:photos/ui/common/loading_widget.dart";
|
||||
import "package:photos/ui/viewer/file/preview_status_widget.dart";
|
||||
import "package:photos/utils/standalone/date_time.dart";
|
||||
@@ -44,6 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isPlayingStreamSubscription =
|
||||
widget.controller.player.stream.playing.listen((isPlaying) {
|
||||
if (isPlaying && !_isSeekingNotifier.value) {
|
||||
@@ -55,7 +57,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
});
|
||||
|
||||
_isSeekingNotifier.addListener(isSeekingListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -131,27 +132,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
widget.file.caption != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
16,
|
||||
12,
|
||||
16,
|
||||
8,
|
||||
),
|
||||
child: Text(
|
||||
widget.file.caption!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
PreviewStatusWidget(
|
||||
showControls: value,
|
||||
file: widget.file,
|
||||
@@ -161,6 +141,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
SeekBarAndDuration(
|
||||
controller: widget.controller,
|
||||
isSeekingNotifier: _isSeekingNotifier,
|
||||
file: widget.file,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -272,11 +253,13 @@ class _PlayPauseButtonState extends State<PlayPauseButtonMediaKit> {
|
||||
class SeekBarAndDuration extends StatelessWidget {
|
||||
final VideoController? controller;
|
||||
final ValueNotifier<bool> isSeekingNotifier;
|
||||
final EnteFile file;
|
||||
|
||||
const SeekBarAndDuration({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.isSeekingNotifier,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -302,46 +285,73 @@ class SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: controller?.player.stream.position,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Text(
|
||||
"0:00",
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
secondsToDuration(snapshot.data!.inSeconds),
|
||||
file.caption != null && file.caption!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
12,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
child: Text(
|
||||
file.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Row(
|
||||
children: [
|
||||
StreamBuilder(
|
||||
stream: controller?.player.stream.position,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Text(
|
||||
"0:00",
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
secondsToDuration(snapshot.data!.inSeconds),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
isSeekingNotifier,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_secondsToDuration(
|
||||
controller!.player.state.duration.inSeconds,
|
||||
),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
isSeekingNotifier,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_secondsToDuration(
|
||||
controller!.player.state.duration.inSeconds,
|
||||
),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import "package:media_kit/media_kit.dart";
|
||||
import "package:media_kit_video/media_kit_video.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/guest_view_event.dart";
|
||||
import "package:photos/events/pause_video_event.dart";
|
||||
import "package:photos/events/stream_switched_event.dart";
|
||||
@@ -60,6 +61,8 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
late final StreamSubscription<GuestViewEvent> _guestViewEventSubscription;
|
||||
bool _isGuestView = false;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -84,6 +87,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_isGuestView = event.isGuestView;
|
||||
});
|
||||
});
|
||||
|
||||
_streamSwitchedSubscription =
|
||||
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
|
||||
if (event.type != PlayerType.mediaKit || !mounted) return;
|
||||
@@ -93,6 +97,15 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
loadOriginal();
|
||||
}
|
||||
});
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == widget.file.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loadPreview() {
|
||||
@@ -147,6 +160,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_progressNotifier.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
player.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import "package:logging/logging.dart";
|
||||
import "package:native_video_player/native_video_player.dart";
|
||||
import "package:photos/core/constants.dart";
|
||||
import "package:photos/core/event_bus.dart";
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/guest_view_event.dart";
|
||||
import "package:photos/events/pause_video_event.dart";
|
||||
import "package:photos/events/seekbar_triggered_event.dart";
|
||||
@@ -80,6 +81,8 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
final _elTooltipController = ElTooltipController();
|
||||
StreamSubscription<PlaybackEvent>? _subscription;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
int position = 0;
|
||||
|
||||
@override
|
||||
@@ -114,6 +117,15 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
loadOriginal(update: true);
|
||||
}
|
||||
});
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == widget.file.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> setVideoSource() async {
|
||||
@@ -207,6 +219,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
_isSeeking.dispose();
|
||||
_debouncer.cancelDebounceTimer();
|
||||
_elTooltipController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -357,6 +370,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
showControls: _showControls,
|
||||
isSeeking: _isSeeking,
|
||||
position: position,
|
||||
file: widget.file,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
@@ -644,6 +658,7 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
final ValueNotifier<bool> showControls;
|
||||
final ValueNotifier<bool> isSeeking;
|
||||
final int position;
|
||||
final EnteFile file;
|
||||
|
||||
const _SeekBarAndDuration({
|
||||
required this.controller,
|
||||
@@ -651,6 +666,7 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
required this.showControls,
|
||||
required this.isSeeking,
|
||||
required this.position,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -691,34 +707,61 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
secondsToDuration(position ~/ 1000),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
file.caption != null && file.caption!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
durationToSeconds(duration),
|
||||
isSeeking,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
duration ?? "0:00",
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, file);
|
||||
},
|
||||
child: Text(
|
||||
file.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Row(
|
||||
children: [
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
secondsToDuration(position ~/ 1000),
|
||||
style: getEnteTextTheme(
|
||||
context,
|
||||
).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
durationToSeconds(duration),
|
||||
isSeeking,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
duration ?? "0:00",
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -748,115 +791,85 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: Platform.isAndroid
|
||||
? MainAxisAlignment.spaceBetween
|
||||
: MainAxisAlignment.center,
|
||||
children: [
|
||||
file.caption?.isNotEmpty ?? false
|
||||
? Expanded(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Text(
|
||||
file.caption!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(color: textBaseDark),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
return Platform.isAndroid && !selectedPreview
|
||||
? Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: ElTooltip(
|
||||
padding: const EdgeInsets.all(12),
|
||||
distance: 4,
|
||||
controller: elTooltipController,
|
||||
content: GestureDetector(
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
elTooltipController.hide();
|
||||
},
|
||||
child: Text(S.of(context).useDifferentPlayerInfo),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Platform.isAndroid && !selectedPreview
|
||||
? ValueListenableBuilder(
|
||||
valueListenable: showControls,
|
||||
builder: (context, value, _) {
|
||||
return IgnorePointer(
|
||||
ignoring: !value,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInQuad,
|
||||
opacity: value ? 1 : 0,
|
||||
child: ElTooltip(
|
||||
padding: const EdgeInsets.all(12),
|
||||
distance: 4,
|
||||
controller: elTooltipController,
|
||||
content: GestureDetector(
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
position: ElTooltipPosition.topEnd,
|
||||
color: backgroundElevatedDark,
|
||||
appearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
disappearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (elTooltipController.value ==
|
||||
ElTooltipStatus.hidden) {
|
||||
elTooltipController.show();
|
||||
} else {
|
||||
elTooltipController.hide();
|
||||
},
|
||||
child: Text(S.of(context).useDifferentPlayerInfo),
|
||||
),
|
||||
position: ElTooltipPosition.topEnd,
|
||||
color: backgroundElevatedDark,
|
||||
appearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
disappearAnimationDuration: const Duration(
|
||||
milliseconds: 200,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (elTooltipController.value ==
|
||||
ElTooltipStatus.hidden) {
|
||||
elTooltipController.show();
|
||||
} else {
|
||||
elTooltipController.hide();
|
||||
}
|
||||
controller?.pause();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_arrow_outlined,
|
||||
size: 24,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
Icon(
|
||||
Icons.question_mark_rounded,
|
||||
size: 10,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
controller?.pause();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: () {
|
||||
Bus.instance.fire(
|
||||
UseMediaKitForVideo(),
|
||||
);
|
||||
HapticFeedback.vibrate();
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_arrow_outlined,
|
||||
size: 24,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
Icon(
|
||||
Icons.question_mark_rounded,
|
||||
size: 10,
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/db/files_db.dart';
|
||||
import "package:photos/events/file_caption_updated_event.dart";
|
||||
import "package:photos/events/files_updated_event.dart";
|
||||
import 'package:photos/events/local_photos_updated_event.dart';
|
||||
import "package:photos/models/file/extensions/file_props.dart";
|
||||
import 'package:photos/models/file/file.dart';
|
||||
import "package:photos/states/detail_page_state.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import "package:photos/ui/actions/file/file_actions.dart";
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/utils/file_util.dart';
|
||||
@@ -55,6 +59,8 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
final _scaleStateController = PhotoViewScaleStateController();
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -70,12 +76,22 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
debugPrint("isZooming = $_isZooming, currentState $value");
|
||||
// _logger.info('is reakky zooming $_isZooming with state $value');
|
||||
};
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == _photo.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_photoViewController.dispose();
|
||||
_scaleStateController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -167,7 +183,68 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
};
|
||||
return GestureDetector(
|
||||
onVerticalDragUpdate: verticalDragCallback,
|
||||
child: content,
|
||||
child: widget.photo.caption?.isNotEmpty ?? false
|
||||
? Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
content,
|
||||
Positioned(
|
||||
bottom: 72 + MediaQuery.paddingOf(context).bottom,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: InheritedDetailPageState.of(context)
|
||||
.enableFullScreenNotifier,
|
||||
builder: (context, doNotShowCaption, _) {
|
||||
return AnimatedOpacity(
|
||||
opacity: doNotShowCaption ? 0.0 : 1.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IgnorePointer(
|
||||
ignoring: doNotShowCaption,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
showDetailsSheet(context, widget.photo);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0,
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: SizedBox(
|
||||
width:
|
||||
MediaQuery.sizeOf(context).width - 16,
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.photo.caption!,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: getEnteTextTheme(context)
|
||||
.mini
|
||||
.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,37 +134,14 @@ class GalleryState extends State<Gallery> {
|
||||
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
|
||||
bool shouldReloadFromDB = true;
|
||||
if (event.source == 'uploadCompleted') {
|
||||
final Map<int, EnteFile> genIDToUploadedFiles = {};
|
||||
for (int i = 0; i < event.updatedFiles.length; i++) {
|
||||
if (event.updatedFiles[i].generatedID == null) {
|
||||
shouldReloadFromDB = true;
|
||||
break;
|
||||
}
|
||||
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
|
||||
event.updatedFiles[i];
|
||||
}
|
||||
for (int i = 0; i < _allGalleryFiles.length; i++) {
|
||||
final file = _allGalleryFiles[i];
|
||||
if (file.generatedID == null) {
|
||||
continue;
|
||||
}
|
||||
final updateFile = genIDToUploadedFiles[file.generatedID!];
|
||||
if (updateFile != null &&
|
||||
updateFile.localID == file.localID &&
|
||||
areFromSameDay(
|
||||
updateFile.creationTime ?? 0,
|
||||
file.creationTime ?? 0,
|
||||
)) {
|
||||
_allGalleryFiles[i] = updateFile;
|
||||
genIDToUploadedFiles.remove(file.generatedID!);
|
||||
}
|
||||
}
|
||||
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
|
||||
shouldReloadFromDB = _shouldReloadOnUploadCompleted(event);
|
||||
} else if (event.source == 'fileMissingLocal') {
|
||||
shouldReloadFromDB = _shouldReloadOnFileMissingLocal(event);
|
||||
}
|
||||
if (!shouldReloadFromDB) {
|
||||
final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles);
|
||||
_logger.info(
|
||||
'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState',
|
||||
'Skip softRefresh from DB on ${event.reason}, processed updated in memory with setStateReload $hasCalledSetState',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -231,6 +208,90 @@ class GalleryState extends State<Gallery> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldReloadOnUploadCompleted(FilesUpdatedEvent event) {
|
||||
bool shouldReloadFromDB = true;
|
||||
if (event.source == 'uploadCompleted') {
|
||||
final Map<int, EnteFile> genIDToUploadedFiles = {};
|
||||
for (int i = 0; i < event.updatedFiles.length; i++) {
|
||||
// matching happens on generatedID and localID
|
||||
if (event.updatedFiles[i].generatedID == null) {
|
||||
return true;
|
||||
}
|
||||
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
|
||||
event.updatedFiles[i];
|
||||
}
|
||||
for (int i = 0; i < _allGalleryFiles.length; i++) {
|
||||
final file = _allGalleryFiles[i];
|
||||
if (file.generatedID == null) {
|
||||
continue;
|
||||
}
|
||||
final updateFile = genIDToUploadedFiles[file.generatedID!];
|
||||
if (updateFile != null &&
|
||||
updateFile.localID == file.localID &&
|
||||
areFromSameDay(
|
||||
updateFile.creationTime ?? 0,
|
||||
file.creationTime ?? 0,
|
||||
)) {
|
||||
_allGalleryFiles[i] = updateFile;
|
||||
genIDToUploadedFiles.remove(file.generatedID!);
|
||||
}
|
||||
}
|
||||
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
|
||||
}
|
||||
return shouldReloadFromDB;
|
||||
}
|
||||
|
||||
// Handle event when an local file was already uploaded and we have now
|
||||
// added localID link link to the remote file
|
||||
bool _shouldReloadOnFileMissingLocal(FilesUpdatedEvent event) {
|
||||
bool shouldReloadFromDB = true;
|
||||
if (event.source != 'fileMissingLocal' ||
|
||||
event.type != EventType.deletedFromEverywhere) {
|
||||
_logger.warning(
|
||||
"Invalid event source or type for fileMissingLocal: ${event.source} ${event.type}",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
final Map<int, EnteFile> genIDToUploadedFiles = {};
|
||||
for (int i = 0; i < event.updatedFiles.length; i++) {
|
||||
// the file should have generatedID, localID and should not be uploaded for
|
||||
// following logic to work
|
||||
if (event.updatedFiles[i].generatedID == null ||
|
||||
event.updatedFiles[i].localID == null ||
|
||||
event.updatedFiles[i].isUploaded) {
|
||||
_logger.warning(
|
||||
"Invalid file in updatedFiles: ${event.updatedFiles[i].localID} ${event.updatedFiles[i].generatedID} ${event.updatedFiles[i].isUploaded}",
|
||||
);
|
||||
return shouldReloadFromDB;
|
||||
}
|
||||
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
|
||||
event.updatedFiles[i];
|
||||
}
|
||||
final List<EnteFile> newAllGalleryFiles = [];
|
||||
for (int i = 0; i < _allGalleryFiles.length; i++) {
|
||||
final file = _allGalleryFiles[i];
|
||||
if (file.generatedID == null) {
|
||||
newAllGalleryFiles.add(file);
|
||||
continue;
|
||||
}
|
||||
final updateFile = genIDToUploadedFiles[file.generatedID!];
|
||||
if (updateFile != null &&
|
||||
areFromSameDay(
|
||||
updateFile.creationTime ?? 0,
|
||||
file.creationTime ?? 0,
|
||||
)) {
|
||||
genIDToUploadedFiles.remove(file.generatedID!);
|
||||
} else {
|
||||
newAllGalleryFiles.add(file);
|
||||
}
|
||||
}
|
||||
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
|
||||
if (!shouldReloadFromDB) {
|
||||
_allGalleryFiles = newAllGalleryFiles;
|
||||
}
|
||||
return shouldReloadFromDB;
|
||||
}
|
||||
|
||||
// group files into multiple groups and returns `true` if it resulted in a
|
||||
// gallery reload
|
||||
bool _onFilesLoaded(List<EnteFile> files) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import 'package:photos/models/collection/collection.dart';
|
||||
import "package:photos/models/selected_files.dart";
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/services/collections_service.dart";
|
||||
import "package:photos/services/filter/db_filters.dart";
|
||||
import "package:photos/theme/colors.dart";
|
||||
@@ -25,7 +26,6 @@ import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import "package:photos/ui/viewer/gallery/gallery.dart";
|
||||
import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
||||
|
||||
Future<dynamic> showAddPhotosSheet(
|
||||
@@ -191,7 +191,8 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is StateError) {
|
||||
final PermissionState ps = await requestPhotoMangerPermissions();
|
||||
final PermissionState ps =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
if (ps != PermissionState.authorized && ps != PermissionState.limited) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
|
||||
@@ -14,7 +14,7 @@ Future<bool> isAndroidSDKVersionLowerThan(int inputSDK) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getDeviceName() async {
|
||||
Future<String?> getDeviceInfo() async {
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||
@@ -22,7 +22,7 @@ Future<String?> getDeviceName() async {
|
||||
} else if (Platform.isAndroid) {
|
||||
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
|
||||
return "${androidInfo.brand} ${androidInfo.model}";
|
||||
return "${androidInfo.brand} ${androidInfo.model} Android ${androidInfo.version.release}";
|
||||
} else {
|
||||
return "Not iOS or Android";
|
||||
}
|
||||
|
||||
@@ -237,11 +237,9 @@ Future<bool> editFileCaption(
|
||||
caption,
|
||||
showDoneToast: false,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (context != null) {
|
||||
showShortToast(context, S.of(context).somethingWentWrong);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
|
||||
Future<PermissionState> requestPhotoMangerPermissions() {
|
||||
return PhotoManager.requestPermissionExtend(
|
||||
requestOption: const PermissionRequestOption(
|
||||
androidPermission: AndroidPermission(
|
||||
type: RequestType.common,
|
||||
mediaLocation: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.9.99+1012
|
||||
version: 0.9.101+1014
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -42,7 +42,7 @@ func NewOfferController(
|
||||
blackFridayOffers := make(ente.BlackFridayOfferPerCountry)
|
||||
path, err := config.BillingConfigFilePath("black-friday.json")
|
||||
if err != nil {
|
||||
log.Fatalf("Error getting offer config file: %v", err)
|
||||
log.Fatalf("Skipping BF configuration, config file not found: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
@@ -88,7 +88,7 @@ func parsePricingFile(fileName string) ente.BillingPlansPerCountry {
|
||||
}
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading file %s: %v\n", filePath, err)
|
||||
logrus.Errorf("Skipping payment configuration, pricing data unavailable in config: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
thirdparty/
|
||||
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"objectWrap": "collapse",
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["packages/base/locales/**/*.json"],
|
||||
"options": { "objectWrap": "preserve" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,12 +12,6 @@ To know more about Ente, see [our main README](../README.md) or visit
|
||||
|
||||
## Building from source
|
||||
|
||||
Fetch submodules
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```sh
|
||||
|
||||
@@ -127,12 +127,7 @@ const Page = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
redirectURL,
|
||||
passkeySessionID,
|
||||
clientPackage,
|
||||
beginResponse,
|
||||
};
|
||||
return { redirectURL, passkeySessionID, clientPackage, beginResponse };
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,9 +139,7 @@ interface BeginPasskeyRegistrationResponse {
|
||||
* Options that should be passed to `navigator.credential.create` when
|
||||
* creating the new {@link Credential}.
|
||||
*/
|
||||
options: {
|
||||
publicKey: PublicKeyCredentialCreationOptions;
|
||||
};
|
||||
options: { publicKey: PublicKeyCredentialCreationOptions };
|
||||
}
|
||||
|
||||
const beginPasskeyRegistration = async (token: string) => {
|
||||
@@ -299,11 +297,7 @@ const finishPasskeyRegistration = async ({
|
||||
// anyways for transmission, we can just reuse the same string.
|
||||
rawId: credential.id,
|
||||
type: credential.type,
|
||||
response: {
|
||||
attestationObject,
|
||||
clientDataJSON,
|
||||
transports,
|
||||
},
|
||||
response: { attestationObject, clientDataJSON, transports },
|
||||
}),
|
||||
});
|
||||
ensureOk(res);
|
||||
@@ -379,9 +373,7 @@ export interface BeginPasskeyAuthenticationResponse {
|
||||
* Options that should be passed to `navigator.credential.get` to obtain the
|
||||
* attested {@link Credential}.
|
||||
*/
|
||||
options: {
|
||||
publicKey: PublicKeyCredentialRequestOptions;
|
||||
};
|
||||
options: { publicKey: PublicKeyCredentialRequestOptions };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.get_login_creds"],
|
||||
"target": {
|
||||
"namespace": "photos-web",
|
||||
"site": "https://web.ente.io"
|
||||
}
|
||||
"target": { "namespace": "photos-web", "site": "https://web.ente.io" }
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.get_login_creds"],
|
||||
"target": {
|
||||
"namespace": "auth-web",
|
||||
"site": "https://auth.ente.io"
|
||||
}
|
||||
"target": { "namespace": "auth-web", "site": "https://auth.ente.io" }
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.get_login_creds"],
|
||||
|
||||
@@ -290,9 +290,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
|
||||
}
|
||||
|
||||
case "steam": {
|
||||
const steam = new Steam({
|
||||
secret: code.secret,
|
||||
});
|
||||
const steam = new Steam({ secret: code.secret });
|
||||
otp = steam.generate();
|
||||
nextOTP = steam.generate({
|
||||
timestamp: Date.now() + code.period * 1000,
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@/build-config": "*",
|
||||
"@types/chromecast-caf-receiver": "^6.0.17"
|
||||
"@types/chromecast-caf-receiver": "^6.0.21"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,7 @@ const registerDevice = async (publicKey: string) => {
|
||||
const res = await fetch(await apiURL("/cast/device-info"), {
|
||||
method: "POST",
|
||||
headers: publicRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
publicKey,
|
||||
}),
|
||||
body: JSON.stringify({ publicKey }),
|
||||
});
|
||||
ensureOk(res);
|
||||
return z.object({ deviceCode: z.string() }).parse(await res.json())
|
||||
|
||||
@@ -166,9 +166,7 @@ const getEncryptedCollectionFiles = async (
|
||||
resp = await HTTPService.get(
|
||||
await apiURL("/cast/diff"),
|
||||
{ sinceTime },
|
||||
{
|
||||
"X-Cast-Access-Token": castToken,
|
||||
},
|
||||
{ "X-Cast-Access-Token": castToken },
|
||||
);
|
||||
const diff = resp.data.diff;
|
||||
files = files.concat(diff.filter((file: EnteFile) => !file.isDeleted));
|
||||
@@ -328,9 +326,7 @@ const downloadFile = async (
|
||||
? `https://cast-albums.ente.io/preview/?fileID=${file.id}`
|
||||
: `https://cast-albums.ente.io/download/?fileID=${file.id}`;
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
"X-Cast-Access-Token": castToken,
|
||||
},
|
||||
headers: { "X-Cast-Access-Token": castToken },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@/build-config": "*",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.11"
|
||||
"vite": "^6.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,7 @@ const stripePublishableKey = (accountCountry: StripeAccountCountry) => {
|
||||
/** Return the {@link StripeAccountCountry} for the user. */
|
||||
const getUserStripeAccountCountry = async (paymentToken: string) => {
|
||||
const url = `${apiOrigin}/billing/stripe-account-country`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"X-Auth-Token": paymentToken,
|
||||
},
|
||||
});
|
||||
const res = await fetch(url, { headers: { "X-Auth-Token": paymentToken } });
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
const json: unknown = await res.json();
|
||||
if (json && typeof json === "object" && "stripeAccountCountry" in json) {
|
||||
@@ -138,11 +134,7 @@ const createCheckoutSession = async (
|
||||
): Promise<string> => {
|
||||
const params = new URLSearchParams({ productID, redirectURL });
|
||||
const url = `${apiOrigin}/billing/stripe/checkout-session?${params.toString()}`;
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"X-Auth-Token": paymentToken,
|
||||
},
|
||||
});
|
||||
const res = await fetch(url, { headers: { "X-Auth-Token": paymentToken } });
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
const json: unknown = await res.json();
|
||||
if (json && typeof json == "object" && "sessionID" in json) {
|
||||
@@ -230,12 +222,8 @@ async function updateStripeSubscription(
|
||||
const url = `${apiOrigin}/billing/stripe/update-subscription`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"X-Auth-Token": paymentToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
productID,
|
||||
}),
|
||||
headers: { "X-Auth-Token": paymentToken },
|
||||
body: JSON.stringify({ productID }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
const json: unknown = await res.json();
|
||||
|
||||