Compare commits
61 Commits
docs-addiO
...
fdroid-v0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b377217ece | ||
|
|
7242176243 | ||
|
|
b3123a6440 | ||
|
|
f4eb511beb | ||
|
|
1a689b2c19 | ||
|
|
b0c6ffdbb2 | ||
|
|
b7ccf4aaf9 | ||
|
|
e7c8265ae1 | ||
|
|
21dc35355d | ||
|
|
f86994b1d3 | ||
|
|
260a26d45c | ||
|
|
cdfa368a8c | ||
|
|
d67c6aef53 | ||
|
|
6ebb5d5bf4 | ||
|
|
224b79b648 | ||
|
|
7e0a3cdd6c | ||
|
|
f6db381e20 | ||
|
|
f0c29fef5c | ||
|
|
2a3e317725 | ||
|
|
1a1b3ebf12 | ||
|
|
f995589a02 | ||
|
|
6e0990d658 | ||
|
|
4da4261f4c | ||
|
|
0abe66ea8c | ||
|
|
193b27a186 | ||
|
|
e323096172 | ||
|
|
e41f306ac8 | ||
|
|
01d45d7c14 | ||
|
|
d55a29336f | ||
|
|
cfcbd0fbb2 | ||
|
|
21174548b5 | ||
|
|
910f13e9a8 | ||
|
|
762688db28 | ||
|
|
9df1ea0c57 | ||
|
|
e48ab71fa4 | ||
|
|
246314367a | ||
|
|
ad70bbb571 | ||
|
|
3962c55140 | ||
|
|
82e478bb12 | ||
|
|
63c8e98492 | ||
|
|
ae92d2f759 | ||
|
|
761c3e6ac2 | ||
|
|
f9a3009c60 | ||
|
|
ca0474faca | ||
|
|
b469985277 | ||
|
|
2a5dacb460 | ||
|
|
d16f98cf07 | ||
|
|
8677cbb4f8 | ||
|
|
0e33299863 | ||
|
|
93ba4e011a | ||
|
|
7977bebcaa | ||
|
|
f28f49d724 | ||
|
|
d9a93ddad6 | ||
|
|
07808d6139 | ||
|
|
1e1633bb45 | ||
|
|
c0f33de0c8 | ||
|
|
417621b17c | ||
|
|
8322540732 | ||
|
|
2d61be37bb | ||
|
|
2a10aa7d61 | ||
|
|
004eb310b3 |
2
.github/workflows/web-deploy-one.yml
vendored
@@ -26,6 +26,8 @@ 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,6 +26,8 @@ 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,6 +34,7 @@ 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,6 +30,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
7
.gitmodules
vendored
@@ -9,3 +9,10 @@
|
||||
[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
|
||||
[submodule "mobile/thirdparty/flutter"]
|
||||
path = mobile/thirdparty/flutter
|
||||
url = https://github.com/flutter/flutter
|
||||
|
||||
@@ -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 the [community section](https://ente.io/about#community) for all the ways to
|
||||
connect with our community.
|
||||
Please visit our [community page](https://ente.io/community) for all the ways to
|
||||
connect with the community.
|
||||
|
||||
[](https://discord.gg/z2YVKkycX3)
|
||||
[](https://ente.io/blog/rss.xml)
|
||||
|
||||
@@ -379,14 +379,6 @@
|
||||
{
|
||||
"title": "Fastmail"
|
||||
},
|
||||
{
|
||||
"title": "Federal Student Aid",
|
||||
"slug": "federal_student_aid",
|
||||
"altNames": [
|
||||
"FSA",
|
||||
"FAFSA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fidelity",
|
||||
"slug": "fidelity",
|
||||
@@ -491,9 +483,6 @@
|
||||
"title": "IceDrive",
|
||||
"slug": "ice_drive"
|
||||
},
|
||||
{
|
||||
"title": "ICONOMI"
|
||||
},
|
||||
{
|
||||
"title": "ID.me",
|
||||
"slug": "id_me"
|
||||
@@ -604,11 +593,6 @@
|
||||
{
|
||||
"title": "Letterboxd"
|
||||
},
|
||||
{
|
||||
"title": "LinkedIn",
|
||||
"slug": "linkedin",
|
||||
"hex": "2596be"
|
||||
},
|
||||
{
|
||||
"title": "Linux.Do",
|
||||
"slug": "linux_do",
|
||||
@@ -630,14 +614,6 @@
|
||||
"title": "Login.gov",
|
||||
"slug": "login_gov"
|
||||
},
|
||||
{
|
||||
"title": "Luma",
|
||||
"slug": "luma",
|
||||
"altNames": [
|
||||
"luma",
|
||||
"lu.ma"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Marketplace.tf",
|
||||
"slug": "marketplacedottf"
|
||||
@@ -667,9 +643,6 @@
|
||||
"MercadoLivre"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "MEXC"
|
||||
},
|
||||
{
|
||||
"title": "microsoft"
|
||||
},
|
||||
@@ -979,10 +952,6 @@
|
||||
{
|
||||
"title": "RuneMate"
|
||||
},
|
||||
{
|
||||
"title": "RuneScape Wiki",
|
||||
"slug": "runescape_wiki"
|
||||
},
|
||||
{
|
||||
"title": "Rust Language Forum",
|
||||
"slug": "rust_language_forum",
|
||||
|
||||
|
Before Width: | Height: | Size: 8.9 KiB |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 750 B |
@@ -1 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,15 +0,0 @@
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -499,7 +499,6 @@
|
||||
"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",
|
||||
@@ -510,7 +509,6 @@
|
||||
"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,7 +499,6 @@
|
||||
"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,7 +499,6 @@
|
||||
"appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。",
|
||||
"duplicateCodes": "重複コード",
|
||||
"noDuplicates": "✨ 重複なし",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません",
|
||||
"deduplicateCodes": "重複コード",
|
||||
"deselectAll": "すべての選択を解除",
|
||||
"selectAll": "すべて選択",
|
||||
|
||||
@@ -499,7 +499,6 @@
|
||||
"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,7 +1,4 @@
|
||||
{
|
||||
"account": "അക്കൗണ്ട്",
|
||||
"unlock": "അൺലോക്ക്",
|
||||
"qrCode": "QR കോഡ്",
|
||||
"blog": "ബ്ലോഗ്",
|
||||
"verifyPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക",
|
||||
"recreatePassword": "പാസ്വേഡ് പുനഃസൃഷ്ടിക്കുക",
|
||||
|
||||
@@ -88,8 +88,6 @@
|
||||
"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",
|
||||
@@ -158,7 +156,6 @@
|
||||
"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",
|
||||
@@ -260,10 +257,6 @@
|
||||
"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.",
|
||||
@@ -334,8 +327,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"manualSort": "Po meri",
|
||||
"editOrder": "Uredi vrstni red",
|
||||
"mostFrequentlyUsed": "Pogosto uporabljeni",
|
||||
"mostRecentlyUsed": "Nedavno uporabljeno",
|
||||
"activeSessions": "Aktivne seje",
|
||||
@@ -457,8 +448,6 @@
|
||||
"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",
|
||||
@@ -496,21 +485,5 @@
|
||||
"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.",
|
||||
"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"
|
||||
"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."
|
||||
}
|
||||
@@ -267,9 +267,7 @@
|
||||
"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",
|
||||
@@ -281,10 +279,6 @@
|
||||
"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",
|
||||
@@ -298,7 +292,6 @@
|
||||
"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"
|
||||
@@ -316,46 +309,28 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -368,7 +343,6 @@
|
||||
"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,9 +112,8 @@ class Code {
|
||||
String issuer,
|
||||
String secret,
|
||||
CodeDisplay? display,
|
||||
int digits, {
|
||||
Algorithm algorithm = Algorithm.sha1,
|
||||
}) {
|
||||
int digits,
|
||||
) {
|
||||
final String encodedIssuer = Uri.encodeQueryComponent(issuer);
|
||||
return Code(
|
||||
account,
|
||||
@@ -122,10 +121,10 @@ class Code {
|
||||
digits,
|
||||
defaultPeriod,
|
||||
secret,
|
||||
algorithm,
|
||||
Algorithm.sha1,
|
||||
type,
|
||||
0,
|
||||
"otpauth://${type.name}/$issuer:$account?algorithm=${algorithm.name.toUpperCase()}&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
|
||||
"otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
|
||||
display: display ?? CodeDisplay(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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';
|
||||
@@ -39,12 +38,10 @@ 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 = [];
|
||||
@@ -52,7 +49,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
bool isCustomIcon = false;
|
||||
String _customIconID = "";
|
||||
late IconType _iconSrc;
|
||||
late Algorithm _algorithm;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -69,12 +65,6 @@ 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) {
|
||||
@@ -111,8 +101,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
? IconType.simpleIcon
|
||||
: IconType.customIcon;
|
||||
|
||||
_algorithm = widget.code == null ? Algorithm.sha1 : widget.code!.algorithm;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -133,7 +121,6 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
_issuerController.dispose();
|
||||
_accountController.dispose();
|
||||
_notesController.dispose();
|
||||
_digitsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -281,79 +268,6 @@ 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,
|
||||
@@ -408,29 +322,12 @@ 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 ||
|
||||
_digitsController.text.trim().isEmpty ||
|
||||
digits == null) {
|
||||
_secretController.text.trim().isEmpty) {
|
||||
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;
|
||||
@@ -461,8 +358,6 @@ 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 =
|
||||
@@ -503,18 +398,14 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
issuer,
|
||||
secret,
|
||||
display,
|
||||
isStreamCode ? Code.steamDigits : digits!,
|
||||
algorithm: _algorithm,
|
||||
isStreamCode ? Code.steamDigits : Code.defaultDigits,
|
||||
)
|
||||
: 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);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
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,6 +45,7 @@ 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,7 +1,6 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"objectWrap": "collapse",
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
## v1.7.11 (Unreleased)
|
||||
|
||||
- Improved file viewer.
|
||||
- Improved live photo experience.
|
||||
- .
|
||||
|
||||
## v1.7.10
|
||||
|
||||
@@ -10,21 +10,19 @@ To know more about Ente, see [our main README](../README.md) or visit
|
||||
|
||||
## Building from source
|
||||
|
||||
Clone this repository and change to this directory
|
||||
Fetch submodules
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ente-io/ente
|
||||
cd ente/desktop
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Install dependencies (requires Yarn v1):
|
||||
Install dependencies
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
```
|
||||
|
||||
Now you can run in development mode (supports hot reload for the renderer
|
||||
process)
|
||||
Run in development mode (supports hot reload for the renderer process)
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
|
||||
@@ -27,17 +27,23 @@ 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.2",
|
||||
"electron-log": "^5.3.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.4.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
@@ -41,22 +41,23 @@
|
||||
"onnxruntime-node": "^1.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@eslint/js": "^9.19.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.3.1",
|
||||
"electron": "^34.1.1",
|
||||
"electron-builder": "^26.0.0",
|
||||
"eslint": "^9",
|
||||
"prettier": "3.5.3",
|
||||
"prettier": "3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"prettier-plugin-packagejson": "^2.5.8",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.0"
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.23.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"productName": "ente"
|
||||
|
||||
@@ -247,7 +247,12 @@ const registerPrivilegedSchemes = () => {
|
||||
corsEnabled: true,
|
||||
},
|
||||
},
|
||||
{ scheme: "stream", privileges: { supportFetchAPI: true } },
|
||||
{
|
||||
scheme: "stream",
|
||||
privileges: {
|
||||
supportFetchAPI: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,9 +36,17 @@ 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([
|
||||
@@ -57,11 +65,20 @@ 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -79,8 +96,14 @@ 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",
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
@@ -109,7 +132,15 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
|
||||
]),
|
||||
],
|
||||
},
|
||||
{ label: "Help", submenu: [{ label: "Ente Help", click: handleHelp }] },
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{
|
||||
label: "Ente Help",
|
||||
click: handleHelp,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -128,7 +159,13 @@ 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,7 +10,10 @@ class AutoLauncher {
|
||||
|
||||
constructor() {
|
||||
if (process.platform != "darwin") {
|
||||
this.autoLaunch = new AutoLaunch({ name: "ente", isHidden: true });
|
||||
this.autoLaunch = new AutoLaunch({
|
||||
name: "ente",
|
||||
isHidden: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +247,9 @@ 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`);
|
||||
@@ -290,7 +292,9 @@ 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);
|
||||
@@ -312,7 +316,9 @@ 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,7 +84,11 @@ 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,7 +5,9 @@ interface SafeStorageStore {
|
||||
}
|
||||
|
||||
const safeStorageSchema: Schema<SafeStorageStore> = {
|
||||
encryptionKey: { type: "string" },
|
||||
encryptionKey: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
|
||||
export const safeStorageStore = new Store({
|
||||
|
||||
@@ -22,13 +22,30 @@ 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" } },
|
||||
items: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
zipPaths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
zipPaths: { type: "array", items: { type: "string" } },
|
||||
};
|
||||
|
||||
export const uploadStatusStore = new Store({
|
||||
|
||||
@@ -23,7 +23,12 @@ 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,7 +34,10 @@ const watchStoreSchema: Schema<WatchStore> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoredFiles: { type: "array", items: { type: "string" } },
|
||||
ignoredFiles: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -125,12 +125,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
const { writable, readable } = new TransformStream();
|
||||
const stream = await zip.stream(entry);
|
||||
|
||||
// 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);
|
||||
const nodeWritable = Writable.fromWeb(writable);
|
||||
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.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/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/object-schema@^2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -317,6 +317,26 @@
|
||||
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"
|
||||
@@ -334,6 +354,11 @@
|
||||
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"
|
||||
@@ -392,62 +417,62 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.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"
|
||||
"@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"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@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"
|
||||
"@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"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.26.0"
|
||||
"@typescript-eslint/utils" "8.26.0"
|
||||
"@typescript-eslint/typescript-estree" "8.23.0"
|
||||
"@typescript-eslint/utils" "8.23.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@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/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/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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/visitor-keys" "8.26.0"
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
"@typescript-eslint/visitor-keys" "8.23.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -455,22 +480,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.26.0"
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/typescript-estree" "8.26.0"
|
||||
"@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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.26.0"
|
||||
"@typescript-eslint/types" "8.23.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@xmldom/xmldom@^0.8.8":
|
||||
@@ -1223,10 +1248,10 @@ electron-builder@^26.0.0:
|
||||
simple-update-notifier "2.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
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-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-publish@26.0.0:
|
||||
version "26.0.0"
|
||||
@@ -1264,10 +1289,10 @@ electron-updater@^6.4.0:
|
||||
semver "^7.6.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^34.3.1:
|
||||
version "34.3.1"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-34.3.1.tgz#2c337a496d923463a2c7be7eaab191ad8220459b"
|
||||
integrity sha512-Vsgxc4FDGg7hjduKyvTP5qfNDxZHTliZIiWD1HlR5hHXx3BFjyVv3db/uEH1GaCU0KKyeNsBXRwS4WAOMaSH5g==
|
||||
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==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^20.9.0"
|
||||
@@ -2669,18 +2694,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.10:
|
||||
version "2.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7"
|
||||
integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==
|
||||
prettier-plugin-packagejson@^2.5.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==
|
||||
dependencies:
|
||||
sort-package-json "2.15.1"
|
||||
sort-package-json "2.14.0"
|
||||
synckit "0.9.2"
|
||||
|
||||
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==
|
||||
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==
|
||||
|
||||
proc-log@^2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -2990,10 +3015,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.15.1:
|
||||
version "2.15.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e"
|
||||
integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==
|
||||
sort-package-json@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==
|
||||
dependencies:
|
||||
detect-indent "^7.0.1"
|
||||
detect-newline "^4.0.0"
|
||||
@@ -3209,24 +3234,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.26.0:
|
||||
version "8.26.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.26.0.tgz#f44cafdaa6edc99e3612b33b791eb77a56286320"
|
||||
integrity sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==
|
||||
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==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.26.0"
|
||||
"@typescript-eslint/parser" "8.26.0"
|
||||
"@typescript-eslint/utils" "8.26.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.23.0"
|
||||
"@typescript-eslint/parser" "8.23.0"
|
||||
"@typescript-eslint/utils" "8.23.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.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==
|
||||
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==
|
||||
|
||||
undici-types@~6.19.2:
|
||||
version "6.19.8"
|
||||
|
||||
@@ -115,15 +115,4 @@ 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.
|
||||
|
||||
# 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
|
||||
On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password.
|
||||
@@ -22,25 +22,6 @@ 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
|
||||
|
||||
|
Before Width: | Height: | Size: 76 KiB |
@@ -54,9 +54,6 @@ 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,6 +33,7 @@ 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,12 +12,13 @@ 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
|
||||
```
|
||||
@@ -162,7 +163,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: [
|
||||
{
|
||||
@@ -211,16 +212,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,6 +49,7 @@ 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
|
||||
```
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="io.ente.photos">
|
||||
<application android:name="${applicationName}"
|
||||
<application
|
||||
tools:replace="android:label"
|
||||
android:name="${applicationName}"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:usesCleartextTraffic="true"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ext {
|
||||
appCompatVersion = '1.1.0' // for background_fetch
|
||||
appCompatVersion = '1.4.2' // for background_fetch
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -7,10 +7,10 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
// mavenLocal() // for FDroid
|
||||
maven {
|
||||
url "${project(':background_fetch').projectDir}/libs"
|
||||
}
|
||||
mavenLocal() // for FDroid
|
||||
// maven {
|
||||
// url "${project(':background_fetch').projectDir}/libs"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -437,81 +437,81 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2
|
||||
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
|
||||
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b
|
||||
ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
|
||||
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
|
||||
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
|
||||
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
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
|
||||
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
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
|
||||
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
|
||||
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
|
||||
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
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
native_video_player: 5d36066807b680e181473e6890dde643ac85380d
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
|
||||
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
|
||||
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
|
||||
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
|
||||
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
|
||||
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: 0f9bc9adfc0b960e7f3bb5ec67e9a3d8193f3bdb
|
||||
sentry_flutter: 64a43fb39ab4c7f67d8a4cad52b49e22439e58b7
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sentry_flutter: f4a0466dc8855998ffd59378ec33507c7dc32d7b
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
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
|
||||
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
|
||||
|
||||
PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
unawaited(
|
||||
getDeviceInfo().then((info) {
|
||||
$.info("Device Info: $info");
|
||||
getDeviceName().then((name) {
|
||||
$.info("Device name: $name");
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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/sync/import/model.dart";
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import 'package:sqflite/sqlite_api.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import "package:photos/events/event.dart";
|
||||
|
||||
class FileCaptionUpdatedEvent extends Event {
|
||||
final int fileGeneratedID;
|
||||
|
||||
FileCaptionUpdatedEvent(this.fileGeneratedID);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import 'package:background_fetch/background_fetch.dart';
|
||||
import "package:computer/computer.dart";
|
||||
import 'package:ente_crypto/ente_crypto.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:flutter/rendering.dart";
|
||||
@@ -40,8 +39,8 @@ import 'package:photos/services/machine_learning/ml_service.dart';
|
||||
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
|
||||
import 'package:photos/services/memories_service.dart';
|
||||
import "package:photos/services/notification_service.dart";
|
||||
// import 'package:photos/services/push_service.dart';
|
||||
import "package:photos/services/preview_video_store.dart";
|
||||
import 'package:photos/services/push_service.dart';
|
||||
import 'package:photos/services/search_service.dart';
|
||||
import 'package:photos/services/sync/local_sync_service.dart';
|
||||
import 'package:photos/services/sync/remote_sync_service.dart';
|
||||
@@ -274,11 +273,11 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// ignore: unawaited_futures
|
||||
PushService.instance.init().then((_) {
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
});
|
||||
// PushService.instance.init().then((_) {
|
||||
// FirebaseMessaging.onBackgroundMessage(
|
||||
// _firebaseMessagingBackgroundHandler,
|
||||
// );
|
||||
// });
|
||||
}
|
||||
_logger.info("PushService/HomeWidget done $tlog");
|
||||
PreviewVideoStore.instance.init(preferences);
|
||||
@@ -403,35 +402,6 @@ Future<void> _killBGTask([String? taskId]) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
final bool isRunningInFG = await _isRunningInForeground(); // hb
|
||||
final bool isInForeground = AppLifecycleService.instance.isForeground;
|
||||
if (_isProcessRunning) {
|
||||
_logger.info(
|
||||
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground",
|
||||
);
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncActiveProcess');
|
||||
}
|
||||
} else {
|
||||
// App is dead
|
||||
// ignore: unawaited_futures
|
||||
_runWithLogs(
|
||||
() async {
|
||||
_logger.info("Background push received");
|
||||
if (Platform.isIOS) {
|
||||
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
|
||||
}
|
||||
await _init(true, via: 'firebasePush');
|
||||
if (PushService.shouldSync(message)) {
|
||||
await _sync('firebaseBgSyncNoActiveProcess');
|
||||
}
|
||||
},
|
||||
prefix: "[fbg]",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logFGHeartBeatInfo(SharedPreferences prefs) async {
|
||||
final bool isRunningInFG = await _isRunningInForeground();
|
||||
await prefs.reload();
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
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;
|
||||
@@ -20,54 +16,6 @@ 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,7 +1,5 @@
|
||||
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";
|
||||
@@ -22,29 +20,18 @@ 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,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
factory MemoriesCache.fromJson(Map<String, dynamic> json) {
|
||||
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,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +40,6 @@ class MemoriesCache {
|
||||
'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories),
|
||||
'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs),
|
||||
'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs),
|
||||
'baseLocations': BaseLocation.encodeListToJson(baseLocations),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,11 +47,8 @@ class MemoriesCache {
|
||||
return jsonEncode(cache.toJson());
|
||||
}
|
||||
|
||||
static MemoriesCache decodeFromJsonString(
|
||||
String jsonString,
|
||||
Map<int, EnteFile> filesMap,
|
||||
) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap);
|
||||
static MemoriesCache decodeFromJsonString(String jsonString) {
|
||||
return MemoriesCache.fromJson(jsonDecode(jsonString));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,7 @@ extension SectionTypeExtensions on SectionType {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: lau: check if we should sort moment again
|
||||
bool get sortByName =>
|
||||
this != SectionType.face &&
|
||||
this != SectionType.magic &&
|
||||
this != SectionType.moment;
|
||||
bool get sortByName => this != SectionType.face && this != SectionType.magic;
|
||||
|
||||
bool get isEmptyCTAVisible {
|
||||
switch (this) {
|
||||
@@ -246,7 +242,6 @@ 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,7 +11,6 @@ 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";
|
||||
@@ -144,9 +143,3 @@ FaceRecognitionService get faceRecognitionService {
|
||||
_faceRecognitionService ??= FaceRecognitionService();
|
||||
return _faceRecognitionService!;
|
||||
}
|
||||
|
||||
PermissionService? _permissionService;
|
||||
PermissionService get permissionService {
|
||||
_permissionService ??= PermissionService(ServiceLocator.instance.prefs);
|
||||
return _permissionService!;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
@@ -29,6 +28,7 @@ class BillingService {
|
||||
late final _logger = Logger("BillingService");
|
||||
final Dio _enteDio;
|
||||
|
||||
// ignore: unused_field
|
||||
bool _isOnSubscriptionPage = false;
|
||||
|
||||
Future<BillingPlans>? _future;
|
||||
@@ -42,23 +42,6 @@ class BillingService {
|
||||
// await FlutterInappPurchase.instance.initConnection;
|
||||
// FlutterInappPurchase.instance.clearTransactionIOS();
|
||||
// }
|
||||
InAppPurchase.instance.purchaseStream.listen((purchases) {
|
||||
if (_isOnSubscriptionPage) {
|
||||
return;
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
).then((response) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
});
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
InAppPurchase.instance.completePurchase(purchase);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void clearCache() {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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");
|
||||
@@ -128,6 +127,99 @@ 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]
|
||||
@@ -222,3 +314,36 @@ 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,
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,6 @@ 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";
|
||||
@@ -34,8 +33,6 @@ class LocationService {
|
||||
|
||||
List<City> _cities = [];
|
||||
|
||||
List<BaseLocation> baseLocations = [];
|
||||
|
||||
LocationService(this.prefs) {
|
||||
debugPrint('LocationService constructor');
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
|
||||
@@ -147,13 +147,11 @@ class SemanticSearchService {
|
||||
}
|
||||
final textEmbedding = await _getTextEmbedding(query);
|
||||
|
||||
final similarityResults = await _getSimilarities(
|
||||
{query: textEmbedding},
|
||||
minimumSimilarityMap: {
|
||||
query: similarityThreshold ?? kMinimumSimilarityThreshold,
|
||||
},
|
||||
final queryResults = await _getSimilarities(
|
||||
textEmbedding,
|
||||
minimumSimilarity: similarityThreshold,
|
||||
);
|
||||
final queryResults = similarityResults[query]!;
|
||||
|
||||
// print query for top ten scores
|
||||
for (int i = 0; i < min(10, queryResults.length); i++) {
|
||||
final result = queryResults[i];
|
||||
@@ -198,32 +196,18 @@ class SemanticSearchService {
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<Map<String, List<int>>> getMatchingFileIDs(
|
||||
Map<String, double> queryToScore,
|
||||
Future<List<int>> getMatchingFileIDs(
|
||||
String query,
|
||||
double minimumSimilarity,
|
||||
) async {
|
||||
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 textEmbedding = await _getTextEmbedding(query);
|
||||
final queryResults = await _getSimilarities(
|
||||
textEmbeddings,
|
||||
minimumSimilarityMap: minimumSimilarityMap,
|
||||
textEmbedding,
|
||||
minimumSimilarity: minimumSimilarity,
|
||||
);
|
||||
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;
|
||||
final result = <int>[];
|
||||
for (final r in queryResults) {
|
||||
result.add(r.id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -265,25 +249,24 @@ class SemanticSearchService {
|
||||
return textEmbedding;
|
||||
}
|
||||
|
||||
Future<Map<String, List<QueryResult>>> _getSimilarities(
|
||||
Map<String, List<double>> textQueryToEmbeddingMap, {
|
||||
required Map<String, double> minimumSimilarityMap,
|
||||
Future<List<QueryResult>> _getSimilarities(
|
||||
List<double> textEmbedding, {
|
||||
double? minimumSimilarity,
|
||||
}) async {
|
||||
final startTime = DateTime.now();
|
||||
final imageEmbeddings = await _getClipVectors();
|
||||
final Map<String, List<QueryResult>> queryResults = await _computer
|
||||
.compute<Map<String, dynamic>, Map<String, List<QueryResult>>>(
|
||||
final List<QueryResult> queryResults = await _computer.compute(
|
||||
computeBulkSimilarities,
|
||||
param: {
|
||||
"imageEmbeddings": imageEmbeddings,
|
||||
"textQueryToEmbeddingMap": textQueryToEmbeddingMap,
|
||||
"minimumSimilarityMap": minimumSimilarityMap,
|
||||
"textEmbedding": textEmbedding,
|
||||
"minimumSimilarity": minimumSimilarity,
|
||||
},
|
||||
taskName: "computeBulkSimilarities",
|
||||
);
|
||||
final endTime = DateTime.now();
|
||||
_logger.info(
|
||||
"computingSimilarities took for ${textQueryToEmbeddingMap.length} queries " +
|
||||
"computingSimilarities took: " +
|
||||
(endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch)
|
||||
.toString() +
|
||||
"ms",
|
||||
@@ -310,44 +293,39 @@ class SemanticSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<QueryResult>> computeBulkSimilarities(Map args) {
|
||||
List<QueryResult> computeBulkSimilarities(Map args) {
|
||||
final queryResults = <QueryResult>[];
|
||||
final imageEmbeddings = args["imageEmbeddings"] as List<EmbeddingVector>;
|
||||
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");
|
||||
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));
|
||||
}
|
||||
}
|
||||
queryResults.sort((first, second) => second.score.compareTo(first.score));
|
||||
result[query] = queryResults;
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
queryResults.sort((first, second) => second.score.compareTo(first.score));
|
||||
return queryResults;
|
||||
}
|
||||
|
||||
class QueryResult {
|
||||
|
||||
@@ -393,17 +393,14 @@ 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) {
|
||||
queryToScore[prompt.query] = prompt.minScore;
|
||||
}
|
||||
final clipResults =
|
||||
await SemanticSearchService.instance.getMatchingFileIDs(queryToScore);
|
||||
for (Prompt prompt in magicPromptsData) {
|
||||
final List<int> fileUploadedIDs = clipResults[prompt.query] ?? [];
|
||||
final fileUploadedIDs =
|
||||
await SemanticSearchService.instance.getMatchingFileIDs(
|
||||
prompt.query,
|
||||
prompt.minScore,
|
||||
);
|
||||
if (fileUploadedIDs.isNotEmpty) {
|
||||
results.add(
|
||||
MagicCache(prompt.title, fileUploadedIDs),
|
||||
@@ -411,7 +408,7 @@ class MagicCacheService {
|
||||
}
|
||||
matchCount.add(fileUploadedIDs.length);
|
||||
}
|
||||
_logger.info('magic result count $matchCount $t');
|
||||
_logger.info('magic result count $matchCount');
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,27 +140,26 @@ class MemoriesCacheService {
|
||||
// calculate memories for this period and for the next period
|
||||
final now = DateTime.now();
|
||||
final next = now.add(kMemoriesUpdateFrequency);
|
||||
final nowResult = await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextResult =
|
||||
final nowMemories =
|
||||
await smartMemoriesService.calcMemories(now, newCache);
|
||||
final nextMemories =
|
||||
await smartMemoriesService.calcMemories(next, newCache);
|
||||
w?.log("calculated new memories");
|
||||
for (final nowMemory in nowResult.memories) {
|
||||
for (final nowMemory in nowMemories) {
|
||||
newCache.toShowMemories
|
||||
.add(ToShowMemory.fromSmartMemory(nowMemory, now));
|
||||
}
|
||||
for (final nextMemory in nextResult.memories) {
|
||||
for (final nextMemory in nextMemories) {
|
||||
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 =
|
||||
nowResult.memories.where((memory) => memory.shouldShowNow()).toList();
|
||||
locationService.baseLocations = nowResult.baseLocations;
|
||||
nowMemories.where((memory) => memory.shouldShowNow()).toList();
|
||||
await file.writeAsBytes(
|
||||
MemoriesCache.encodeToJsonString(newCache).codeUnits,
|
||||
);
|
||||
@@ -175,14 +174,8 @@ 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) {
|
||||
@@ -228,10 +221,9 @@ class MemoriesCacheService {
|
||||
}
|
||||
}
|
||||
return MemoriesCache(
|
||||
toShowMemories: [],
|
||||
toShowMemories: toShowMemories,
|
||||
peopleShownLogs: peopleShownLogs,
|
||||
tripsShownLogs: tripsShownLogs,
|
||||
baseLocations: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,7 +259,6 @@ class MemoriesCacheService {
|
||||
);
|
||||
}
|
||||
}
|
||||
locationService.baseLocations = cache.baseLocations;
|
||||
_logger.info('Processing of disk cache memories done');
|
||||
return memories;
|
||||
} catch (e, s) {
|
||||
@@ -303,17 +294,8 @@ 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, allFileIdsToFile);
|
||||
return MemoriesCache.decodeFromJsonString(jsonString);
|
||||
}
|
||||
|
||||
Future<void> clearMemoriesCache() async {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/core/constants.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/core/network/network.dart';
|
||||
import 'package:photos/events/signed_in_event.dart';
|
||||
import 'package:photos/services/sync/sync_service.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PushService {
|
||||
static const kFCMPushToken = "fcm_push_token";
|
||||
static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
|
||||
static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
|
||||
static const kPushAction = "action";
|
||||
static const kSync = "sync";
|
||||
|
||||
static final PushService instance = PushService._privateConstructor();
|
||||
static final _logger = Logger("PushService");
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
PushService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await Firebase.initializeApp();
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
_logger.info("Got a message whilst in the foreground!");
|
||||
_handleForegroundPushMessage(message);
|
||||
});
|
||||
try {
|
||||
if (Configuration.instance.hasConfiguredAccount()) {
|
||||
await _configurePushToken();
|
||||
} else {
|
||||
Bus.instance.on<SignedInEvent>().listen((_) async {
|
||||
// ignore: unawaited_futures
|
||||
_configurePushToken();
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe("Could not configure push token", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _configurePushToken() async {
|
||||
final String? fcmToken = await FirebaseMessaging.instance.getToken();
|
||||
final shouldForceRefreshServerToken =
|
||||
DateTime.now().microsecondsSinceEpoch -
|
||||
(_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
|
||||
kFCMTokenUpdationIntervalInMicroSeconds;
|
||||
if (fcmToken != null &&
|
||||
(_prefs.getString(kFCMPushToken) != fcmToken ||
|
||||
shouldForceRefreshServerToken)) {
|
||||
final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
try {
|
||||
_logger.info("Updating token on server");
|
||||
await _setPushTokenOnServer(fcmToken, apnsToken);
|
||||
await _prefs.setString(kFCMPushToken, fcmToken);
|
||||
await _prefs.setInt(
|
||||
kLastFCMTokenUpdationTime,
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
);
|
||||
_logger.info("Push token updated on server");
|
||||
} catch (e) {
|
||||
_logger.severe("Could not set push token", e, StackTrace.current);
|
||||
}
|
||||
} else {
|
||||
_logger.info("Skipping token update");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setPushTokenOnServer(
|
||||
String fcmToken,
|
||||
String? apnsToken,
|
||||
) async {
|
||||
await NetworkClient.instance.enteDio.post(
|
||||
"/push/token",
|
||||
data: {
|
||||
"fcmToken": fcmToken,
|
||||
"apnsToken": apnsToken,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleForegroundPushMessage(RemoteMessage message) {
|
||||
_logger.info("Message data: ${message.data}");
|
||||
if (message.notification != null) {
|
||||
_logger.info(
|
||||
"Message also contained a notification: ${message.notification}",
|
||||
);
|
||||
}
|
||||
if (shouldSync(message)) {
|
||||
SyncService.instance.sync();
|
||||
}
|
||||
}
|
||||
|
||||
static bool shouldSync(RemoteMessage message) {
|
||||
return message.data.containsKey(kPushAction) &&
|
||||
message.data[kPushAction] == kSync;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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";
|
||||
@@ -1049,42 +1048,6 @@ 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, '');
|
||||
@@ -1230,24 +1193,9 @@ class SearchService {
|
||||
BuildContext context,
|
||||
int? limit,
|
||||
) async {
|
||||
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 memories = await memoriesCacheService.getMemories(limit);
|
||||
final searchResults = <GenericSearchResult>[];
|
||||
for (final memory in memoriesResult.memories) {
|
||||
for (final memory in memories) {
|
||||
final files = Memory.filesFromMemories(memory.memories);
|
||||
searchResults.add(
|
||||
GenericSearchResult(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
@@ -10,7 +9,6 @@ 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";
|
||||
@@ -36,13 +34,6 @@ 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;
|
||||
@@ -82,58 +73,45 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// One general method to get all memories, which calls on internal methods for each separate memory type
|
||||
Future<MemoriesResult> calcMemories(
|
||||
Future<List<SmartMemory>> calcMemories(
|
||||
DateTime now,
|
||||
MemoriesCache oldCache, {
|
||||
bool debugSurfaceAll = false,
|
||||
}) async {
|
||||
MemoriesCache oldCache,
|
||||
) async {
|
||||
try {
|
||||
final TimeLogger t = TimeLogger(context: "calcMemories");
|
||||
_logger.finest('calcMemories called with time: $now $t');
|
||||
_logger.finest('calcMemories called with time: $now');
|
||||
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} $t");
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
|
||||
final peopleMemories = await _getPeopleResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.peopleShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
final peopleMemories =
|
||||
await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs);
|
||||
_deductUsedMemories(allFiles, peopleMemories);
|
||||
memories.addAll(peopleMemories);
|
||||
_logger.finest("All files length after people: ${allFiles.length} $t");
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
|
||||
// Trip memories
|
||||
final (tripMemories, bases) = await _getTripsResults(
|
||||
allFiles,
|
||||
now,
|
||||
oldCache.tripsShownLogs,
|
||||
surfaceAll: debugSurfaceAll,
|
||||
);
|
||||
final tripMemories = await _getTripsResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, tripMemories);
|
||||
memories.addAll(tripMemories);
|
||||
_logger.finest("All files length after trips: ${allFiles.length} $t");
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
|
||||
// Time memories
|
||||
final timeMemories = await _onThisDayOrWeekResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, timeMemories);
|
||||
memories.addAll(timeMemories);
|
||||
_logger.finest("All files length after time: ${allFiles.length} $t");
|
||||
_logger.finest("All files length: ${allFiles.length}");
|
||||
|
||||
// Filler memories
|
||||
final fillerMemories = await _getFillerResults(allFiles, now);
|
||||
_deductUsedMemories(allFiles, fillerMemories);
|
||||
memories.addAll(fillerMemories);
|
||||
_logger.finest("All files length after filler: ${allFiles.length} $t");
|
||||
return MemoriesResult(memories, bases);
|
||||
return memories;
|
||||
} catch (e, s) {
|
||||
_logger.severe("Error calculating smart memories", e, s);
|
||||
return MemoriesResult(<SmartMemory>[], <BaseLocation>[]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +129,8 @@ class SmartMemoriesService {
|
||||
Future<List<PeopleMemory>> _getPeopleResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
List<PeopleShownLog> shownPeople, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
|
||||
List<PeopleShownLog> shownPeople,
|
||||
) async {
|
||||
final List<PeopleMemory> memoryResults = [];
|
||||
if (allFiles.isEmpty) return [];
|
||||
final allFileIdsToFile = <int, EnteFile>{};
|
||||
@@ -166,7 +142,6 @@ 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();
|
||||
@@ -194,7 +169,6 @@ class SmartMemoriesService {
|
||||
final bFaces = personIdToFaceIDs[b]!.length;
|
||||
return bFaces.compareTo(aFaces);
|
||||
});
|
||||
w?.log('orderedImportantPersonsID setup');
|
||||
|
||||
// Check if the user has assignmed "me"
|
||||
String? meID;
|
||||
@@ -205,7 +179,6 @@ class SmartMemoriesService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
w?.log('meID setup part 1');
|
||||
final bool isMeAssigned = meID != null;
|
||||
Map<int, List<Face>>? meFilesToFaces;
|
||||
if (isMeAssigned) {
|
||||
@@ -214,7 +187,6 @@ class SmartMemoriesService {
|
||||
meFileIDs,
|
||||
);
|
||||
}
|
||||
w?.log('meID setup part 2');
|
||||
|
||||
// Loop through the people and find all memories
|
||||
final Map<String, Map<PeopleMemoryType, PeopleMemory>> personToMemories =
|
||||
@@ -222,12 +194,10 @@ 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) {
|
||||
@@ -258,7 +228,6 @@ class SmartMemoriesService {
|
||||
.putIfAbsent(personID, () => {})
|
||||
.putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory);
|
||||
}
|
||||
w?.log('spotlight setup');
|
||||
|
||||
// Inside people loop, check for youAndThem
|
||||
if (isMeAssigned && meID != personID) {
|
||||
@@ -289,14 +258,12 @@ 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:
|
||||
@@ -310,9 +277,6 @@ 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;
|
||||
@@ -343,7 +307,6 @@ class SmartMemoriesService {
|
||||
() => activityMemory,
|
||||
);
|
||||
}
|
||||
w?.log('doingSomethingTogether setup');
|
||||
}
|
||||
|
||||
// Inside people loop, check for lastTimeYouSawThem
|
||||
@@ -392,19 +355,16 @@ class SmartMemoriesService {
|
||||
() => lastTimeMemory,
|
||||
);
|
||||
}
|
||||
w?.log('lastTimeYouSawThem setup');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// // 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]!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Loop through the people and check if we should surface anything based on relevancy (bday, last met)
|
||||
personRelevancyLoop:
|
||||
@@ -472,7 +432,6 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
}
|
||||
w?.log('relevancy setup');
|
||||
|
||||
// Loop through the people (and memory types) and add based on rotation
|
||||
if (memoryResults.length >= 3) return memoryResults;
|
||||
@@ -512,30 +471,24 @@ class SmartMemoriesService {
|
||||
}
|
||||
if (added > 0) break peopleRotationLoop;
|
||||
}
|
||||
w?.log('rotation setup');
|
||||
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
Future<(List<TripMemory>, List<BaseLocation>)> _getTripsResults(
|
||||
Future<List<TripMemory>> _getTripsResults(
|
||||
Iterable<EnteFile> allFiles,
|
||||
DateTime currentTime,
|
||||
List<TripsShownLog> shownTrips, {
|
||||
bool surfaceAll = false,
|
||||
}) async {
|
||||
) async {
|
||||
final List<TripMemory> memoryResults = [];
|
||||
final Iterable<LocalEntity<LocationTag>> locationTagEntities =
|
||||
(await locationService.getLocationTags());
|
||||
if (allFiles.isEmpty) return (<TripMemory>[], <BaseLocation>[]);
|
||||
if (allFiles.isEmpty) return [];
|
||||
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)] = [];
|
||||
@@ -543,13 +496,12 @@ 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 allFilesLoop;
|
||||
continue;
|
||||
}
|
||||
// Check if the file is inside any location tag
|
||||
bool hasLocationTag = false;
|
||||
@@ -564,40 +516,41 @@ class SmartMemoriesService {
|
||||
}
|
||||
}
|
||||
// Cluster the files not inside any location tag (incremental clustering)
|
||||
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 (!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 (!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 (!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 (!addedToExistingWideCluster) {
|
||||
wideRadiusClusters.add(([file], file.location!));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,20 +577,12 @@ class SmartMemoriesService {
|
||||
final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch(
|
||||
creationTimes.last,
|
||||
);
|
||||
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
|
||||
if (daysRange < 90) {
|
||||
if (lastCreationTime.difference(firstCreationTime).inDays < 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(
|
||||
@@ -659,7 +604,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
baseLocation.location,
|
||||
location,
|
||||
overlapRadius,
|
||||
10.0,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -669,7 +614,7 @@ class SmartMemoriesService {
|
||||
if (isFileInsideLocationTag(
|
||||
tag.item.centerPoint,
|
||||
location,
|
||||
overlapRadius,
|
||||
10.0,
|
||||
)) {
|
||||
tooClose = true;
|
||||
break;
|
||||
@@ -808,51 +753,25 @@ class SmartMemoriesService {
|
||||
}
|
||||
|
||||
// For now for testing let's just surface all base locations
|
||||
// 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(
|
||||
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(
|
||||
Memory.fromFiles(baseLocation.files, _seenTimes),
|
||||
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);
|
||||
name,
|
||||
0,
|
||||
0,
|
||||
baseLocation.location,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip
|
||||
@@ -925,14 +844,17 @@ 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(6, (i) => ((currentMonth + i) % 12) + 1);
|
||||
List<int>.generate(12, (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) {
|
||||
thatMonthTrips.addAll(trips);
|
||||
for (final trip in trips) {
|
||||
thatMonthTrips.add(trip);
|
||||
}
|
||||
}
|
||||
if (thatMonthTrips.length >= 3) {
|
||||
// take and use the third earliest trip
|
||||
@@ -940,46 +862,32 @@ class SmartMemoriesService {
|
||||
(a, b) =>
|
||||
a.averageCreationTime().compareTo(b.averageCreationTime()),
|
||||
);
|
||||
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 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";
|
||||
}
|
||||
final photoSelection = await _bestSelection(trip.memories);
|
||||
memoryResults.add(
|
||||
trip.copyWith(
|
||||
memories: photoSelection,
|
||||
title: name,
|
||||
firstDateToShow: nowInMicroseconds,
|
||||
lastDateToShow: windowEnd,
|
||||
),
|
||||
);
|
||||
break checkUpcomingMonths;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (memoryResults, baseLocations);
|
||||
return memoryResults;
|
||||
}
|
||||
|
||||
Future<List<TimeMemory>> _onThisDayOrWeekResults(
|
||||
@@ -1313,7 +1221,6 @@ 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;
|
||||
@@ -1408,7 +1315,6 @@ 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);
|
||||
@@ -1448,9 +1354,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>{};
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
class LocalPathAsset {
|
||||
final Set<String> localIDs;
|
||||
final String pathID;
|
||||
final String pathName;
|
||||
|
||||
LocalPathAsset({
|
||||
required this.localIDs,
|
||||
required this.pathName,
|
||||
required this.pathID,
|
||||
});
|
||||
}
|
||||
@@ -13,17 +13,14 @@ 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/sync/import/diff.dart";
|
||||
import "package:photos/services/sync/import/local_assets.dart";
|
||||
import "package:photos/services/sync/import/model.dart";
|
||||
import 'package:photos/services/local/local_sync_util.dart';
|
||||
import "package:photos/utils/photo_manager_util.dart";
|
||||
import "package:photos/utils/standalone/debouncer.dart";
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
@@ -39,6 +36,8 @@ 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();
|
||||
|
||||
@@ -50,23 +49,18 @@ class LocalSyncService {
|
||||
if (!AppLifecycleService.instance.isForeground) {
|
||||
await PhotoManager.setIgnorePermissionCheck(true);
|
||||
}
|
||||
if (permissionService.hasGrantedPermissions()) {
|
||||
if (hasGrantedPermissions()) {
|
||||
_registerChangeCallback();
|
||||
} else {
|
||||
Bus.instance.on<PermissionGrantedEvent>().listen((event) async {
|
||||
_registerChangeCallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sync() async {
|
||||
if (!permissionService.hasGrantedPermissions()) {
|
||||
if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
|
||||
_logger.info("Skipping local sync since permission has not been granted");
|
||||
return;
|
||||
}
|
||||
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
|
||||
final permissionState =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
final permissionState = await requestPhotoMangerPermissions();
|
||||
if (permissionState != PermissionState.authorized) {
|
||||
_logger.severe(
|
||||
"sync requested with invalid permission",
|
||||
@@ -174,7 +168,7 @@ class LocalSyncService {
|
||||
final Map<String, Set<String>> pathToLocalIDs =
|
||||
await _db.getDevicePathIDToLocalIDMap();
|
||||
|
||||
final localDiffResult = await getDiffFromExistingImport(
|
||||
final localDiffResult = await getDiffWithLocal(
|
||||
localAssets,
|
||||
existingLocalFileIDs,
|
||||
pathToLocalIDs,
|
||||
@@ -243,6 +237,36 @@ 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;
|
||||
}
|
||||
@@ -341,7 +365,7 @@ class LocalSyncService {
|
||||
if (_existingSync != null) {
|
||||
await _existingSync!.future;
|
||||
}
|
||||
if (permissionService.hasGrantedLimitedPermissions()) {
|
||||
if (hasGrantedLimitedPermissions()) {
|
||||
unawaited(syncAll());
|
||||
} else {
|
||||
unawaited(sync().then((value) => _refreshDeviceFolderCountAndCover()));
|
||||
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
@@ -168,7 +169,10 @@ class SyncService {
|
||||
return _lastSyncStatusEvent;
|
||||
}
|
||||
|
||||
Future<void> onPermissionGranted() async {
|
||||
Future<void> onPermissionGranted(PermissionState state) async {
|
||||
_logger.info("Permission granted " + state.toString());
|
||||
await _localSyncService.onPermissionGranted(state);
|
||||
Bus.instance.fire(PermissionGrantedEvent());
|
||||
_doSync().ignore();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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,11 +5,13 @@ 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/service_locator.dart";
|
||||
import "package:photos/services/sync/local_sync_service.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});
|
||||
@@ -46,20 +48,20 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
|
||||
onTap: () async {
|
||||
try {
|
||||
final PermissionState state =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
await permissionService.onUpdatePermission(state);
|
||||
await requestPhotoMangerPermissions();
|
||||
await LocalSyncService.instance.onUpdatePermission(state);
|
||||
} on Exception catch (e) {
|
||||
Logger("HomeHeaderWidget").severe(
|
||||
"Failed to request permission: ${e.toString()}",
|
||||
e,
|
||||
);
|
||||
}
|
||||
if (!permissionService.hasGrantedFullPermission()) {
|
||||
if (!LocalSyncService.instance.hasGrantedFullPermission()) {
|
||||
if (Platform.isAndroid) {
|
||||
await PhotoManager.openSetting();
|
||||
} else {
|
||||
final bool hasGrantedLimit =
|
||||
permissionService.hasGrantedLimitedPermissions();
|
||||
LocalSyncService.instance.hasGrantedLimitedPermissions();
|
||||
// ignore: unawaited_futures
|
||||
showChoiceActionSheet(
|
||||
context,
|
||||
|
||||
@@ -4,15 +4,12 @@ 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 {
|
||||
@@ -109,12 +106,11 @@ class _GrantPermissionsWidgetState extends State<GrantPermissionsWidget> {
|
||||
child: Text(S.of(context).grantPermission),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final state =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
final state = await requestPhotoMangerPermissions();
|
||||
_logger.info("Permission state: $state");
|
||||
if (state == PermissionState.authorized ||
|
||||
state == PermissionState.limited) {
|
||||
await onPermissionGranted(state);
|
||||
await SyncService.instance.onPermissionGranted(state);
|
||||
} else if (state == PermissionState.denied) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
@@ -143,16 +139,4 @@ 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/service_locator.dart";
|
||||
import 'package:photos/services/sync/local_sync_service.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 (permissionService.hasGrantedLimitedPermissions()) {
|
||||
if (LocalSyncService.instance.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/service_locator.dart";
|
||||
import 'package:photos/services/sync/local_sync_service.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,7 +42,8 @@ class StartBackupHookWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: GradientButton(
|
||||
onTap: () async {
|
||||
if (permissionService.hasGrantedLimitedPermissions()) {
|
||||
if (LocalSyncService.instance
|
||||
.hasGrantedLimitedPermissions()) {
|
||||
unawaited(PhotoManager.presentLimited());
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/errors.dart';
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/events/subscription_purchased_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/models/api/billing/billing_plan.dart';
|
||||
import 'package:photos/models/api/billing/subscription.dart';
|
||||
import 'package:photos/models/user_details.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import 'package:photos/services/account/user_service.dart';
|
||||
import "package:photos/theme/colors.dart";
|
||||
import "package:photos/theme/ente_theme.dart";
|
||||
import 'package:photos/ui/common/loading_widget.dart';
|
||||
import 'package:photos/ui/common/progress_dialog.dart';
|
||||
import "package:photos/ui/components/captioned_text_widget.dart";
|
||||
import "package:photos/ui/components/divider_widget.dart";
|
||||
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
|
||||
import "package:photos/ui/components/title_bar_title_widget.dart";
|
||||
import 'package:photos/ui/notification/toast.dart';
|
||||
import 'package:photos/ui/payment/child_subscription_widget.dart';
|
||||
import 'package:photos/ui/payment/subscription_common_widgets.dart';
|
||||
import 'package:photos/ui/payment/subscription_plan_widget.dart';
|
||||
import "package:photos/ui/payment/view_add_on_widget.dart";
|
||||
import "package:photos/ui/tabs/home_widget.dart";
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
import "package:photos/utils/standalone/data.dart";
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class StoreSubscriptionPage extends StatefulWidget {
|
||||
final bool isOnboarding;
|
||||
|
||||
const StoreSubscriptionPage({
|
||||
this.isOnboarding = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StoreSubscriptionPage> createState() => _StoreSubscriptionPageState();
|
||||
}
|
||||
|
||||
class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
|
||||
final _logger = Logger("SubscriptionPage");
|
||||
late final _billingService = billingService;
|
||||
final _userService = UserService.instance;
|
||||
Subscription? _currentSubscription;
|
||||
late StreamSubscription _purchaseUpdateSubscription;
|
||||
late ProgressDialog _dialog;
|
||||
late UserDetails _userDetails;
|
||||
late bool _hasActiveSubscription;
|
||||
bool _hideCurrentPlanSelection = false;
|
||||
late FreePlan _freePlan;
|
||||
late List<BillingPlan> _plans;
|
||||
bool _hasLoadedData = false;
|
||||
bool _isLoading = false;
|
||||
late bool _isActiveStripeSubscriber;
|
||||
EnteColorScheme colorScheme = darkScheme;
|
||||
|
||||
// hasYearlyPlans is used to check if there are yearly plans for given store
|
||||
bool hasYearlyPlans = false;
|
||||
|
||||
// _showYearlyPlan is used to determine if we should show the yearly plans
|
||||
bool showYearlyPlan = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_billingService.setIsOnSubscriptionPage(true);
|
||||
_setupPurchaseUpdateStreamListener();
|
||||
}
|
||||
|
||||
void _setupPurchaseUpdateStreamListener() {
|
||||
_purchaseUpdateSubscription =
|
||||
InAppPurchase.instance.purchaseStream.listen((purchases) async {
|
||||
if (!_dialog.isShowing()) {
|
||||
await _dialog.show();
|
||||
}
|
||||
for (final purchase in purchases) {
|
||||
_logger.info("Purchase status " + purchase.status.toString());
|
||||
if (purchase.status == PurchaseStatus.purchased) {
|
||||
try {
|
||||
final newSubscription = await _billingService.verifySubscription(
|
||||
purchase.productID,
|
||||
purchase.verificationData.serverVerificationData,
|
||||
);
|
||||
await InAppPurchase.instance.completePurchase(purchase);
|
||||
String text = S.of(context).thankYouForSubscribing;
|
||||
if (!widget.isOnboarding) {
|
||||
final isUpgrade = _hasActiveSubscription &&
|
||||
newSubscription.storage > _currentSubscription!.storage;
|
||||
final isDowngrade = _hasActiveSubscription &&
|
||||
newSubscription.storage < _currentSubscription!.storage;
|
||||
if (isUpgrade) {
|
||||
text = S.of(context).yourPlanWasSuccessfullyUpgraded;
|
||||
} else if (isDowngrade) {
|
||||
text = S.of(context).yourPlanWasSuccessfullyDowngraded;
|
||||
}
|
||||
}
|
||||
showShortToast(context, text);
|
||||
_currentSubscription = newSubscription;
|
||||
_hasActiveSubscription = _currentSubscription!.isValid();
|
||||
setState(() {});
|
||||
await _dialog.hide();
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
if (widget.isOnboarding) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} on SubscriptionAlreadyClaimedError catch (e) {
|
||||
_logger.warning("subscription is already claimed ", e);
|
||||
await _dialog.hide();
|
||||
final String title = Platform.isAndroid
|
||||
? S.of(context).playstoreSubscription
|
||||
: S.of(context).appstoreSubscription;
|
||||
final String id = Platform.isAndroid
|
||||
? S.of(context).googlePlayId
|
||||
: S.of(context).appleId;
|
||||
final String message = S.of(context).subAlreadyLinkedErrMessage(id);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(context, title, message);
|
||||
return;
|
||||
} catch (e) {
|
||||
_logger.warning("Could not complete payment ", e);
|
||||
await _dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).paymentFailed,
|
||||
S.of(context).paymentFailedTalkToProvider(
|
||||
Platform.isAndroid ? "PlayStore" : "AppStore",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
|
||||
await InAppPurchase.instance.completePurchase(purchase);
|
||||
await _dialog.hide();
|
||||
} else if (purchase.status == PurchaseStatus.error) {
|
||||
await _dialog.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_purchaseUpdateSubscription.cancel();
|
||||
_billingService.setIsOnSubscriptionPage(false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = getEnteTextTheme(context);
|
||||
colorScheme = getEnteColorScheme(context);
|
||||
if (!_isLoading) {
|
||||
_isLoading = true;
|
||||
_fetchSubData();
|
||||
}
|
||||
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TitleBarTitleWidget(
|
||||
title: widget.isOnboarding
|
||||
? S.of(context).selectYourPlan
|
||||
: "${S.of(context).subscription}${kDebugMode ? ' Store' : ''}",
|
||||
),
|
||||
_isFreePlanUser() || !_hasLoadedData
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
convertBytesToReadableFormat(
|
||||
_userDetails.getTotalStorage(),
|
||||
),
|
||||
style: textTheme.smallMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _getBody()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isFreePlanUser() {
|
||||
return _currentSubscription != null &&
|
||||
freeProductID == _currentSubscription!.productID;
|
||||
}
|
||||
|
||||
Future<void> _fetchSubData() async {
|
||||
// ignore: unawaited_futures
|
||||
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
|
||||
_userDetails = userDetails;
|
||||
_currentSubscription = userDetails.subscription;
|
||||
|
||||
_hasActiveSubscription = _currentSubscription!.isValid();
|
||||
_hideCurrentPlanSelection =
|
||||
_currentSubscription?.attributes?.isCancelled ?? false;
|
||||
showYearlyPlan = _currentSubscription!.isYearlyPlan();
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_isActiveStripeSubscriber =
|
||||
_currentSubscription!.paymentProvider == stripe &&
|
||||
_currentSubscription!.isValid();
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
final productID = _isActiveStripeSubscriber
|
||||
? plan.stripeID
|
||||
: Platform.isAndroid
|
||||
? plan.androidID
|
||||
: plan.iosID;
|
||||
return productID.isNotEmpty;
|
||||
}).toList();
|
||||
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
|
||||
if (showYearlyPlan && hasYearlyPlans) {
|
||||
_plans = _plans.where((plan) => plan.period == 'year').toList();
|
||||
} else {
|
||||
_plans = _plans.where((plan) => plan.period != 'year').toList();
|
||||
}
|
||||
_freePlan = billingPlans.freePlan;
|
||||
_hasLoadedData = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
if (_hasLoadedData) {
|
||||
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
|
||||
return ChildSubscriptionWidget(userDetails: _userDetails);
|
||||
} else {
|
||||
return _buildPlans();
|
||||
}
|
||||
}
|
||||
return const EnteLoadingWidget();
|
||||
}
|
||||
|
||||
Widget _buildPlans() {
|
||||
final widgets = <Widget>[];
|
||||
widgets.add(
|
||||
SubscriptionHeaderWidget(
|
||||
isOnboarding: widget.isOnboarding,
|
||||
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
|
||||
),
|
||||
);
|
||||
|
||||
if (hasYearlyPlans) {
|
||||
widgets.add(
|
||||
SubscriptionToggle(
|
||||
onToggle: (p0) {
|
||||
showYearlyPlan = p0;
|
||||
_filterStorePlansForUi();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
widgets.addAll([
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _isActiveStripeSubscriber
|
||||
? _getStripePlanWidgets()
|
||||
: _getMobilePlanWidgets(),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
]);
|
||||
|
||||
if (_currentSubscription != null) {
|
||||
widgets.add(
|
||||
ValidityWidget(
|
||||
currentSubscription: _currentSubscription,
|
||||
bonusData: _userDetails.bonusData,
|
||||
),
|
||||
);
|
||||
widgets.add(const DividerWidget(dividerType: DividerType.bottomBar));
|
||||
widgets.add(const SizedBox(height: 20));
|
||||
} else {
|
||||
widgets.add(const DividerWidget(dividerType: DividerType.bottomBar));
|
||||
const SizedBox(height: 56);
|
||||
}
|
||||
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription!.productID != freeProductID) {
|
||||
if (_isActiveStripeSubscriber) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
|
||||
child: Text(
|
||||
S.of(context).visitWebToManage,
|
||||
style: getEnteTextTheme(context).small.copyWith(
|
||||
color: colorScheme.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
|
||||
child: MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Manage payment method",
|
||||
),
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
trailingWidget: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: colorScheme.strokeBase,
|
||||
),
|
||||
singleBorderRadius: 4,
|
||||
alignCaptionedTextToLeft: true,
|
||||
onTap: () async {
|
||||
_onPlatformRestrictedPaymentDetailsClick();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
widgets.add(
|
||||
SubFaqWidget(isOnboarding: widget.isOnboarding),
|
||||
);
|
||||
|
||||
if (!widget.isOnboarding) {
|
||||
widgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
|
||||
child: MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: _isFreePlanUser()
|
||||
? S.of(context).familyPlans
|
||||
: S.of(context).manageFamily,
|
||||
),
|
||||
menuItemColor: colorScheme.fillFaint,
|
||||
trailingWidget: Icon(
|
||||
Icons.chevron_right_outlined,
|
||||
color: colorScheme.strokeBase,
|
||||
),
|
||||
singleBorderRadius: 4,
|
||||
alignCaptionedTextToLeft: true,
|
||||
onTap: () async {
|
||||
unawaited(
|
||||
_billingService.launchFamilyPortal(context, _userDetails),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
widgets.add(ViewAddOnButton(_userDetails.bonusData));
|
||||
}
|
||||
|
||||
widgets.add(const SizedBox(height: 80));
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: widgets,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPlatformRestrictedPaymentDetailsClick() {
|
||||
final String paymentProvider = _currentSubscription!.paymentProvider;
|
||||
if (paymentProvider == appStore && !Platform.isAndroid) {
|
||||
launchUrlString("https://apps.apple.com/account/billing");
|
||||
} else if (paymentProvider == playStore && Platform.isAndroid) {
|
||||
launchUrlString(
|
||||
"https://play.google.com/store/account/subscriptions?sku=" +
|
||||
_currentSubscription!.productID +
|
||||
"&package=io.ente.photos",
|
||||
);
|
||||
} else if (paymentProvider == stripe) {
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).visitWebToManage,
|
||||
);
|
||||
} else {
|
||||
final String capitalizedWord = paymentProvider.isNotEmpty
|
||||
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
|
||||
: '';
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).contactToManageSubscription(capitalizedWord),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _filterStorePlansForUi() async {
|
||||
final billingPlans = await _billingService.getBillingPlans();
|
||||
_plans = billingPlans.plans.where((plan) {
|
||||
final productID = _isActiveStripeSubscriber
|
||||
? plan.stripeID
|
||||
: Platform.isAndroid
|
||||
? plan.androidID
|
||||
: plan.iosID;
|
||||
return productID.isNotEmpty;
|
||||
}).toList();
|
||||
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
|
||||
if (showYearlyPlan) {
|
||||
_plans = _plans.where((plan) => plan.period == 'year').toList();
|
||||
} else {
|
||||
_plans = _plans.where((plan) => plan.period != 'year').toList();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
List<Widget> _getStripePlanWidgets() {
|
||||
final List<Widget> planWidgets = [];
|
||||
bool foundActivePlan = false;
|
||||
for (final plan in _plans) {
|
||||
final productID = plan.stripeID;
|
||||
if (productID.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final isActive = _hasActiveSubscription &&
|
||||
_currentSubscription!.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (widget.isOnboarding && plan.id == freeProductID) {
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
unawaited(
|
||||
_billingService.verifySubscription(
|
||||
freeProductID,
|
||||
"",
|
||||
paymentProvider: "ente",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).visitWebToManage,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive && !_hideCurrentPlanSelection,
|
||||
isPopular: _isPopularPlan(plan),
|
||||
isOnboarding: widget.isOnboarding,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
List<Widget> _getMobilePlanWidgets() {
|
||||
bool foundActivePlan = false;
|
||||
final List<Widget> planWidgets = [];
|
||||
if (_hasActiveSubscription &&
|
||||
_currentSubscription!.productID == freeProductID) {
|
||||
foundActivePlan = true;
|
||||
planWidgets.add(
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_currentSubscription!.isFreePlan() && widget.isOnboarding) {
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
unawaited(
|
||||
_billingService.verifySubscription(
|
||||
freeProductID,
|
||||
"",
|
||||
paymentProvider: "ente",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: _freePlan.storage,
|
||||
price: "",
|
||||
period: S.of(context).freeTrial,
|
||||
isActive: true,
|
||||
isOnboarding: widget.isOnboarding,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final plan in _plans) {
|
||||
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
|
||||
final isActive = _hasActiveSubscription &&
|
||||
_currentSubscription!.productID == productID;
|
||||
if (isActive) {
|
||||
foundActivePlan = true;
|
||||
}
|
||||
planWidgets.add(
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
final int addOnBonus =
|
||||
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
|
||||
if (_userDetails.getFamilyOrPersonalUsage() >
|
||||
(plan.storage + addOnBonus)) {
|
||||
_logger.warning(
|
||||
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
|
||||
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
|
||||
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
|
||||
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).sorry,
|
||||
S.of(context).youCannotDowngradeToThisPlan,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _dialog.show();
|
||||
final ProductDetailsResponse response =
|
||||
await InAppPurchase.instance.queryProductDetails({productID});
|
||||
if (response.notFoundIDs.isNotEmpty) {
|
||||
final errMsg =
|
||||
"Could not find products: " + response.notFoundIDs.toString();
|
||||
_logger.severe(errMsg);
|
||||
await _dialog.hide();
|
||||
await showGenericErrorDialog(
|
||||
context: context,
|
||||
error: Exception(errMsg),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final isCrossGradingOnAndroid = Platform.isAndroid &&
|
||||
_hasActiveSubscription &&
|
||||
_currentSubscription!.productID != freeProductID &&
|
||||
_currentSubscription!.productID != plan.androidID;
|
||||
if (isCrossGradingOnAndroid) {
|
||||
await _dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
S.of(context).couldNotUpdateSubscription,
|
||||
S.of(context).pleaseContactSupportAndWeWillBeHappyToHelp,
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
await InAppPurchase.instance.buyNonConsumable(
|
||||
purchaseParam: PurchaseParam(
|
||||
productDetails: response.productDetails[0],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: plan.storage,
|
||||
price: plan.price,
|
||||
period: plan.period,
|
||||
isActive: isActive,
|
||||
isPopular: _isPopularPlan(plan),
|
||||
isOnboarding: widget.isOnboarding,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!foundActivePlan && _hasActiveSubscription) {
|
||||
_addCurrentPlanWidget(planWidgets);
|
||||
}
|
||||
return planWidgets;
|
||||
}
|
||||
|
||||
void _addCurrentPlanWidget(List<Widget> planWidgets) {
|
||||
int activePlanIndex = 0;
|
||||
for (; activePlanIndex < _plans.length; activePlanIndex++) {
|
||||
if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
planWidgets.insert(
|
||||
activePlanIndex,
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (_currentSubscription!.isFreePlan() & widget.isOnboarding) {
|
||||
Bus.instance.fire(SubscriptionPurchasedEvent());
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomeWidget();
|
||||
},
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
unawaited(
|
||||
_billingService.verifySubscription(
|
||||
freeProductID,
|
||||
"",
|
||||
paymentProvider: "ente",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SubscriptionPlanWidget(
|
||||
storage: _currentSubscription!.storage,
|
||||
price: _currentSubscription!.price,
|
||||
period: _currentSubscription!.period,
|
||||
isActive: true,
|
||||
isOnboarding: widget.isOnboarding,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isPopularPlan(BillingPlan plan) {
|
||||
return popularProductIDs.contains(plan.id);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,6 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/service_locator.dart";
|
||||
import "package:photos/ui/payment/store_subscription_page.dart";
|
||||
import 'package:photos/ui/payment/stripe_subscription_page.dart';
|
||||
|
||||
StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
|
||||
if (updateService.isIndependentFlavor()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
if (flagService.enableStripe && _isUserCreatedPostStripeSupport()) {
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
} else {
|
||||
return StoreSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
}
|
||||
|
||||
// return true if the user was created after we added support for stripe payment
|
||||
// on frame. We do this check to avoid showing Stripe payment option for earlier
|
||||
// users who might have paid via playStore. This method should be removed once
|
||||
// we have better handling for active play/app store subscription & stripe plans.
|
||||
bool _isUserCreatedPostStripeSupport() {
|
||||
return Configuration.instance.getUserID()! > 1580559962386460;
|
||||
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
|
||||
}
|
||||
|
||||
@@ -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 (!permissionService.hasGrantedLimitedPermissions()) {
|
||||
if (!LocalSyncService.instance.hasGrantedLimitedPermissions()) {
|
||||
delayInRefresh = const Duration(milliseconds: 250);
|
||||
}
|
||||
Future.delayed(
|
||||
@@ -643,7 +643,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
_closeDrawerIfOpen(context);
|
||||
return const LandingPageWidget();
|
||||
}
|
||||
if (!permissionService.hasGrantedPermissions()) {
|
||||
if (!LocalSyncService.instance.hasGrantedPermissions()) {
|
||||
entityService.syncEntities().then((_) {
|
||||
PersonService.instance.resetEmailToPartialPersonDataCache();
|
||||
});
|
||||
@@ -671,7 +671,7 @@ class _HomeWidgetState extends State<HomeWidget> {
|
||||
|
||||
_showShowBackupHook =
|
||||
!Configuration.instance.hasSelectedAnyBackupFolder() &&
|
||||
!permissionService.hasGrantedLimitedPermissions() &&
|
||||
!LocalSyncService.instance.hasGrantedLimitedPermissions() &&
|
||||
CollectionsService.instance.getActiveCollections().isEmpty;
|
||||
|
||||
return Stack(
|
||||
|
||||
@@ -16,7 +16,6 @@ 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';
|
||||
@@ -78,6 +77,7 @@ 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,6 +103,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
void dispose() {
|
||||
_guestViewEventSubscription.cancel();
|
||||
_pageController.dispose();
|
||||
_enableFullScreenNotifier.dispose();
|
||||
_selectedIndexNotifier.dispose();
|
||||
super.dispose();
|
||||
|
||||
@@ -136,102 +137,96 @@ class _DetailPageState extends State<DetailPage> {
|
||||
_files!.length.toString() +
|
||||
" files .",
|
||||
);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
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,
|
||||
),
|
||||
}
|
||||
},
|
||||
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,
|
||||
),
|
||||
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],
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.threesixty,
|
||||
color: Colors.white,
|
||||
size: 26,
|
||||
),
|
||||
onPressed: () async {
|
||||
await openPanoramaViewerPage(
|
||||
_files![selectedIndex],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -256,7 +251,7 @@ class _DetailPageState extends State<DetailPage> {
|
||||
).ignore();
|
||||
}
|
||||
|
||||
Widget _buildPageView() {
|
||||
Widget _buildPageView(BuildContext context) {
|
||||
return PageView.builder(
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) {
|
||||
@@ -276,17 +271,14 @@ class _DetailPageState extends State<DetailPage> {
|
||||
},
|
||||
playbackCallback: (isPlaying) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
InheritedDetailPageState.of(context)
|
||||
.toggleFullScreen(shouldEnable: isPlaying);
|
||||
_toggleFullScreen(shouldEnable: isPlaying);
|
||||
});
|
||||
},
|
||||
backgroundDecoration: const BoxDecoration(color: Colors.black),
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
file.fileType != FileType.video
|
||||
? InheritedDetailPageState.of(context).toggleFullScreen()
|
||||
: null;
|
||||
file.fileType != FileType.video ? _toggleFullScreen() : null;
|
||||
},
|
||||
child: fileContent,
|
||||
);
|
||||
@@ -321,6 +313,26 @@ 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,6 +100,11 @@ 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,12 +1,9 @@
|
||||
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 {
|
||||
@@ -74,19 +71,18 @@ 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!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
editFileCaption(null, widget.file, editedCaption!);
|
||||
}
|
||||
_textController.dispose();
|
||||
_focusNode.removeListener(_focusNodeListener);
|
||||
@@ -152,8 +148,7 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
|
||||
Future<void> _onDoneClick(BuildContext context) async {
|
||||
if (editedCaption != null) {
|
||||
final isSuccesful =
|
||||
await editFileCaption(context, widget.file, editedCaption!)
|
||||
.then((isSuccess) => _onEditFileFinish(isSuccess));
|
||||
await editFileCaption(context, widget.file, editedCaption!);
|
||||
if (isSuccesful) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
@@ -190,15 +185,4 @@ 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,7 +5,6 @@ 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";
|
||||
@@ -45,7 +44,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isPlayingStreamSubscription =
|
||||
widget.controller.player.stream.playing.listen((isPlaying) {
|
||||
if (isPlaying && !_isSeekingNotifier.value) {
|
||||
@@ -57,6 +55,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
});
|
||||
|
||||
_isSeekingNotifier.addListener(isSeekingListener);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -132,6 +131,27 @@ 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,
|
||||
@@ -141,7 +161,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
SeekBarAndDuration(
|
||||
controller: widget.controller,
|
||||
isSeekingNotifier: _isSeekingNotifier,
|
||||
file: widget.file,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -253,13 +272,11 @@ 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
|
||||
@@ -285,73 +302,46 @@ class SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
child: Row(
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,7 +7,6 @@ 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";
|
||||
@@ -61,8 +60,6 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
late final StreamSubscription<GuestViewEvent> _guestViewEventSubscription;
|
||||
bool _isGuestView = false;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -87,7 +84,6 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_isGuestView = event.isGuestView;
|
||||
});
|
||||
});
|
||||
|
||||
_streamSwitchedSubscription =
|
||||
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
|
||||
if (event.type != PlayerType.mediaKit || !mounted) return;
|
||||
@@ -97,15 +93,6 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
loadOriginal();
|
||||
}
|
||||
});
|
||||
|
||||
_captionUpdatedSubscription =
|
||||
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
|
||||
if (event.fileGeneratedID == widget.file.generatedID) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loadPreview() {
|
||||
@@ -160,7 +147,6 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
|
||||
_progressNotifier.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
player.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -81,8 +80,6 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
final _elTooltipController = ElTooltipController();
|
||||
StreamSubscription<PlaybackEvent>? _subscription;
|
||||
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
int position = 0;
|
||||
|
||||
@override
|
||||
@@ -117,15 +114,6 @@ 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 {
|
||||
@@ -219,7 +207,6 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
_isSeeking.dispose();
|
||||
_debouncer.cancelDebounceTimer();
|
||||
_elTooltipController.dispose();
|
||||
_captionUpdatedSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -370,7 +357,6 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
|
||||
showControls: _showControls,
|
||||
isSeeking: _isSeeking,
|
||||
position: position,
|
||||
file: widget.file,
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
@@ -658,7 +644,6 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
final ValueNotifier<bool> showControls;
|
||||
final ValueNotifier<bool> isSeeking;
|
||||
final int position;
|
||||
final EnteFile file;
|
||||
|
||||
const _SeekBarAndDuration({
|
||||
required this.controller,
|
||||
@@ -666,7 +651,6 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
required this.showControls,
|
||||
required this.isSeeking,
|
||||
required this.position,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -707,61 +691,34 @@ class _SeekBarAndDuration extends StatelessWidget {
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
child: Row(
|
||||
children: [
|
||||
file.caption != null && file.caption!.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
12,
|
||||
AnimatedSize(
|
||||
duration: const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
curve: Curves.easeInOut,
|
||||
child: Text(
|
||||
secondsToDuration(position ~/ 1000),
|
||||
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,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SeekBar(
|
||||
controller!,
|
||||
durationToSeconds(duration),
|
||||
isSeeking,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
duration ?? "0:00",
|
||||
style: getEnteTextTheme(context).mini.copyWith(
|
||||
color: textBaseDark,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -791,85 +748,115 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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 {
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: 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();
|
||||
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),
|
||||
),
|
||||
],
|
||||
},
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,10 @@ 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';
|
||||
@@ -59,8 +55,6 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
bool _isZooming = false;
|
||||
PhotoViewController _photoViewController = PhotoViewController();
|
||||
final _scaleStateController = PhotoViewScaleStateController();
|
||||
late final StreamSubscription<FileCaptionUpdatedEvent>
|
||||
_captionUpdatedSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -76,22 +70,12 @@ 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();
|
||||
}
|
||||
|
||||
@@ -183,68 +167,7 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||
};
|
||||
return GestureDetector(
|
||||
onVerticalDragUpdate: verticalDragCallback,
|
||||
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,
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,14 +134,37 @@ class GalleryState extends State<Gallery> {
|
||||
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
|
||||
bool shouldReloadFromDB = true;
|
||||
if (event.source == 'uploadCompleted') {
|
||||
shouldReloadFromDB = _shouldReloadOnUploadCompleted(event);
|
||||
} else if (event.source == 'fileMissingLocal') {
|
||||
shouldReloadFromDB = _shouldReloadOnFileMissingLocal(event);
|
||||
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;
|
||||
}
|
||||
if (!shouldReloadFromDB) {
|
||||
final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles);
|
||||
_logger.info(
|
||||
'Skip softRefresh from DB on ${event.reason}, processed updated in memory with setStateReload $hasCalledSetState',
|
||||
'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -208,90 +231,6 @@ 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,7 +11,6 @@ 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";
|
||||
@@ -26,6 +25,7 @@ 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,8 +191,7 @@ class AddPhotosPhotoWidget extends StatelessWidget {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is StateError) {
|
||||
final PermissionState ps =
|
||||
await permissionService.requestPhotoMangerPermissions();
|
||||
final PermissionState ps = await requestPhotoMangerPermissions();
|
||||
if (ps != PermissionState.authorized && ps != PermissionState.limited) {
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
|
||||
@@ -14,7 +14,7 @@ Future<bool> isAndroidSDKVersionLowerThan(int inputSDK) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getDeviceInfo() async {
|
||||
Future<String?> getDeviceName() async {
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||
@@ -22,7 +22,7 @@ Future<String?> getDeviceInfo() async {
|
||||
} else if (Platform.isAndroid) {
|
||||
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
|
||||
return "${androidInfo.brand} ${androidInfo.model} Android ${androidInfo.version.release}";
|
||||
return "${androidInfo.brand} ${androidInfo.model}";
|
||||
} else {
|
||||
return "Not iOS or Android";
|
||||
}
|
||||
|
||||
@@ -237,9 +237,11 @@ Future<bool> editFileCaption(
|
||||
caption,
|
||||
showDoneToast: false,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (context != null) {
|
||||
showShortToast(context, S.of(context).somethingWentWrong);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
12
mobile/lib/utils/photo_manager_util.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import "package:photo_manager/photo_manager.dart";
|
||||
|
||||
Future<PermissionState> requestPhotoMangerPermissions() {
|
||||
return PhotoManager.requestPermissionExtend(
|
||||
requestOption: const PermissionRequestOption(
|
||||
androidPermission: AndroidPermission(
|
||||
type: RequestType.common,
|
||||
mediaLocation: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -9,14 +9,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "72.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "401dd18096f5eaa140404ccbbbf346f83c850e6f27049698a7ee75a3488ddb32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.52"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
@@ -203,10 +195,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2"
|
||||
sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.3"
|
||||
version: "8.9.5"
|
||||
cached_network_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -446,10 +438,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
dots_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -687,54 +679,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "6a4ea0f1d533443c8afc3d809cd36a4e2b8f2e2e711f697974f55bb31d71d1b8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: e47f5c2776de018fa19bc9f6f723df136bc75cdb164d64b65305babd715c8e41
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.21.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "8755a083a20bac4485e8b46d223f6f2eab34e659a76a75f8cf3cded53bc98a15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.2.3"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: "8cc771079677460de53ad8fcca5bc3074d58c5fc4f9d89b19585e5bfd9c64292"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.3"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: caa73059b0396c97f691683c4cfc3f897c8543801579b7dd4851c431d8e4e091
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.10.3"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1010,10 +954,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
|
||||
sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.24"
|
||||
version: "2.0.26"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1358,38 +1302,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
in_app_purchase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: in_app_purchase
|
||||
sha256: "11a40f148eeb4f681a0572003e2b33432e110c90c1bbb4f9ef83b81ec0c4f737"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
in_app_purchase_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_android
|
||||
sha256: "45ae4fe253f85b4fcc58b421fe137f6e48aca16bf8a618cd760cb0542e7f854e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.0"
|
||||
in_app_purchase_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_platform_interface
|
||||
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
in_app_purchase_storekit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: in_app_purchase_storekit
|
||||
sha256: "276831961023055b55a2156c1fc043f50f6215ff49fb0f5f2273da6eeb510ecf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.21"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -1527,10 +1439,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
|
||||
sha256: "8bba79f4f0f7bc812fce2ca20915d15618c37721246ba6c3ef2aa7a763a90cf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.46"
|
||||
version: "1.0.47"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1633,7 +1545,7 @@ packages:
|
||||
description:
|
||||
path: media_kit
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.1.11"
|
||||
@@ -1650,7 +1562,7 @@ packages:
|
||||
description:
|
||||
path: "libs/ios/media_kit_libs_ios_video"
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.1.4"
|
||||
@@ -1675,7 +1587,7 @@ packages:
|
||||
description:
|
||||
path: "libs/universal/media_kit_libs_video"
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.0.5"
|
||||
@@ -1692,7 +1604,7 @@ packages:
|
||||
description:
|
||||
path: media_kit_video
|
||||
ref: HEAD
|
||||
resolved-ref: "3c4ff28c43d20e68f8d587956b2f525292c25a80"
|
||||
resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe
|
||||
url: "https://github.com/media-kit/media-kit"
|
||||
source: git
|
||||
version: "1.2.5"
|
||||
@@ -1881,18 +1793,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.1"
|
||||
version: "8.3.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
page_transition:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2171,10 +2083,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.2.0"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2220,10 +2132,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: ff9141bed547db02233e7dd88f990ab01973a0c8a8c04ddb855c7b072f33409a
|
||||
sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2292,10 +2204,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
|
||||
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.6"
|
||||
version: "2.4.7"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2481,18 +2393,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
|
||||
sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.4"
|
||||
version: "2.7.5"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
|
||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.30"
|
||||
version: "0.5.31"
|
||||
sqlite_async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2958,10 +2870,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -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.101+1014
|
||||
version: 0.9.99+1013
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -73,8 +73,6 @@ dependencies:
|
||||
url: https://github.com/aloisdeniel/figma_squircle.git
|
||||
ref: 7cc383b30e96c07acd4e484c1d6731d054f7f6ec
|
||||
file_saver: ^0.2.14
|
||||
firebase_core: ^3.6.0
|
||||
firebase_messaging: ^15.1.3
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_animate: ^4.1.0
|
||||
@@ -107,7 +105,6 @@ dependencies:
|
||||
image: ^4.0.17
|
||||
image_editor: ^1.6.0
|
||||
image_picker: ^1.1.1
|
||||
in_app_purchase: ^3.0.7
|
||||
intl: ^0.19.0
|
||||
json_annotation: ^4.8.0
|
||||
latlong2: ^0.9.0
|
||||
@@ -272,7 +269,7 @@ flutter_icons:
|
||||
android: "launcher_icon"
|
||||
adaptive_icon_foreground: "assets/launcher_icon/ente-icon-foreground.png"
|
||||
adaptive_icon_background: "#ffffff"
|
||||
ios: true
|
||||
ios: false # F-Droid
|
||||
image_path: "assets/icon-light.png"
|
||||
|
||||
flutter_native_splash:
|
||||
|
||||
1
mobile/thirdparty/flutter
vendored
Submodule
19
mobile/thirdparty/transistor-background-fetch/LICENSE
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2017 Transistor Software <info@transistorsoft.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
22
mobile/thirdparty/transistor-background-fetch/README.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
Transistor Background Fetch
|
||||
===========================================================================
|
||||
|
||||
Copyright (c) 2017 Transistor Software <info@transistorsoft.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
28
mobile/thirdparty/transistor-background-fetch/TSBackgroundFetch.podspec
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#
|
||||
# Be sure to run `pod lib lint TSBackgroundFetch.podspec' to ensure this is a
|
||||
# valid spec before submitting.
|
||||
#
|
||||
# Any lines starting with a # are optional, but their use is encouraged
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'TSBackgroundFetch'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'iOS Background Fetch API Manager'
|
||||
|
||||
s.description = <<-DESC
|
||||
iOS Background Fetch API Manager with ability to handle multiple listeners.
|
||||
DESC
|
||||
|
||||
s.homepage = 'http://www.transistorsoft.com'
|
||||
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
||||
s.author = { 'christocracy' => 'christocracy@gmail.com' }
|
||||
s.source = { :git => 'https://github.com/transistorsoft/transistor-background-fetch.git', :tag => s.version.to_s }
|
||||
s.social_media_url = 'https://twitter.com/christocracy'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
|
||||
s.source_files = 'ios/TSBackgroundFetch/TSBackgroundFetch/*.{h,m}'
|
||||
s.vendored_frameworks = 'ios/TSBackgroundFetch/TSBackgroundFetch.framework'
|
||||
end
|
||||
9
mobile/thirdparty/transistor-background-fetch/android/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
1
mobile/thirdparty/transistor-background-fetch/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||