Compare commits

..

61 Commits

Author SHA1 Message Date
Neeraj Gupta
b377217ece Merge branch 'mob_6_march' into f-droid 2025-03-10 15:10:56 +05:30
Neeraj Gupta
7242176243 [mob] Bump version code 2025-03-06 15:46:52 +05:30
Neeraj Gupta
b3123a6440 Merge branch '0.9.98_release_branch' into f-droid 2025-02-14 20:07:06 +05:30
ashilkn
f4eb511beb Merge tag 'photos-v0.9.97' into f-droid 2025-02-12 22:07:36 +05:30
Neeraj Gupta
1a689b2c19 Merge branch 'main' into f-droid 2025-02-10 14:29:21 +05:30
Neeraj Gupta
b0c6ffdbb2 Merge branch 'main' into f-droid 2025-01-15 13:06:56 +05:30
Neeraj Gupta
b7ccf4aaf9 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2025-01-15 13:06:47 +05:30
ashilkn
e7c8265ae1 Merge branch 'main' into f-droid 2025-01-08 12:39:54 +05:30
ashilkn
21dc35355d Merge branch 'main' into f-droid 2025-01-03 18:40:49 +05:30
ashilkn
f86994b1d3 Merge tag 'photos-v0.9.72' into f-droid 2024-12-20 11:44:00 +05:30
Neeraj Gupta
260a26d45c Merge branch 'main' into f-droid 2024-12-11 21:58:29 +05:30
ashilkn
cdfa368a8c Merge branch 'main' into f-droid 2024-12-09 12:51:05 +05:30
Neeraj Gupta
d67c6aef53 Merge branch 'main' into f-droid 2024-11-28 11:01:39 +05:30
Neeraj Gupta
6ebb5d5bf4 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-11-28 11:00:11 +05:30
ashilkn
224b79b648 Merge tag 'photos-v0.9.58' into f-droid 2024-11-08 16:08:08 +05:30
Neeraj Gupta
7e0a3cdd6c Merge branch 'main' into f-droid 2024-10-24 13:29:54 +05:30
ashilkn
f6db381e20 [mob][photos] Resolve merge conflicts and merge main 2024-10-23 11:25:54 +05:30
ashilkn
f0c29fef5c Merge branch 'main' into f-droid 2024-10-16 17:06:01 +05:30
Neeraj Gupta
2a3e317725 Merge branch 'main' into f-droid 2024-10-15 21:01:21 +05:30
ashilkn
1a1b3ebf12 [mob][photos] Resolve merge conflicts and merge main 2024-10-09 13:52:19 +05:30
Neeraj Gupta
f995589a02 Merge branch 'main' into f-droid 2024-09-29 12:04:26 +05:30
Neeraj Gupta
6e0990d658 Merge branch 'main' into f-droid 2024-09-20 15:56:08 +05:30
Neeraj Gupta
4da4261f4c Update flutter to 3.24.3 2024-09-20 15:00:23 +05:30
Neeraj Gupta
0abe66ea8c Merge branch 'main' into f-droid 2024-09-20 14:49:17 +05:30
Neeraj Gupta
193b27a186 Merge commit '0a1e062c' into f-droid 2024-09-06 15:30:52 +05:30
Neeraj Gupta
e323096172 Merge tag 'photos-v0.9.30' into f-droid 2024-08-27 17:20:23 +05:30
ashilkn
e41f306ac8 [mob][photos] Resolve merge conflicts and merge main 2024-07-31 12:02:25 +05:30
Neeraj Gupta
01d45d7c14 Merge branch 'main' into f-droid 2024-07-19 15:53:08 +05:30
ashilkn
d55a29336f Merge branch 'main' into f-droid 2024-07-08 20:50:35 +05:30
Neeraj Gupta
cfcbd0fbb2 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-06-17 11:47:58 +05:30
Neeraj Gupta
21174548b5 Merge branch 'main' into f-droid 2024-06-17 11:47:42 +05:30
Neeraj Gupta
910f13e9a8 [mob][fdroid] Update flutter to v3.22.0 2024-06-17 11:31:36 +05:30
ashilkn
762688db28 Merge branch 'main' into f-droid 2024-06-13 10:29:55 +05:30
ashilkn
9df1ea0c57 Merge branch 'main' into f-droid 2024-06-12 17:33:12 +05:30
ashilkn
e48ab71fa4 [mob][photos] f-droid: upgrade flutter submodule to version 3.22.2 2024-06-12 17:33:02 +05:30
ashilkn
246314367a [mob][photos] Update flutter submodule on f-droid 2024-06-04 13:14:24 +05:30
ashilkn
ad70bbb571 Merge branch 'main' into f-droid 2024-06-04 13:11:17 +05:30
Neeraj Gupta
3962c55140 Update flutter submodule: v3.22.0 2024-06-03 11:26:02 +05:30
Neeraj Gupta
82e478bb12 Merge branch 'f-droid' of https://github.com/ente-io/auth into f-droid 2024-06-03 11:25:26 +05:30
Neeraj Gupta
63c8e98492 Merge branch 'main' into f-droid 2024-06-03 11:21:35 +05:30
ashilkn
ae92d2f759 Merge branch 'main' into f-droid 2024-05-28 12:37:14 +05:30
ashilkn
761c3e6ac2 [mob][photos] Update flutter submodule on f-droid branch 2024-05-28 12:34:37 +05:30
ashilkn
f9a3009c60 [mob][photos] Resolve merge conflicts and merge 2024-05-28 12:28:03 +05:30
Neeraj Gupta
ca0474faca Updated submodule mobile/thirdparty/flutter to 3.22.1 2024-05-23 17:00:33 +05:30
Neeraj Gupta
b469985277 Removed submodule mobile/thirdparty/isar 2024-05-23 16:58:51 +05:30
Neeraj Gupta
2a5dacb460 Merge branch 'main' into f-droid 2024-05-23 16:55:27 +05:30
vishnukvmd
d16f98cf07 v0.8.95 2024-05-12 08:44:26 +05:30
vishnukvmd
8677cbb4f8 Increase JVM allocation pool 2024-05-12 08:43:55 +05:30
vishnukvmd
0e33299863 Merge branch 'main' into f-droid 2024-05-07 12:54:44 +05:30
ashilkn
93ba4e011a Merge branch 'main' into f-droid 2024-04-20 15:23:14 +05:30
vishnukvmd
7977bebcaa Update Flutter to v3.19.3 2024-04-16 11:35:32 +05:30
ashilkn
f28f49d724 Merge main 2024-04-15 11:20:03 +05:30
ashilkn
d9a93ddad6 Merge branch 'main' into f-droid 2024-04-13 15:24:56 +05:30
ashilkn
07808d6139 Merge branch 'main' into f-droid 2024-04-02 17:22:34 +05:30
vishnukvmd
1e1633bb45 Merge branch 'main' into f-droid 2024-03-13 21:57:19 +05:30
vishnukvmd
c0f33de0c8 Remove dead code 2024-03-13 21:56:09 +05:30
vishnukvmd
417621b17c Pull code for transistor-background-fetch 2024-03-13 14:14:19 +05:30
vishnukvmd
8322540732 Add submodule for Flutter 2024-03-13 14:13:40 +05:30
vishnukvmd
2d61be37bb Add submodule for Isar 2024-03-13 14:12:23 +05:30
vishnukvmd
2a10aa7d61 Merge branch 'fdroid_cleanup' into f-droid 2024-03-13 13:52:25 +05:30
vishnukvmd
004eb310b3 Prepare for F-Droid 2024-03-13 13:43:46 +05:30
331 changed files with 8260 additions and 6134 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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.
[![Discord](https://img.shields.io/discord/948937918347608085?style=for-the-badge&logo=Discord&logoColor=white&label=Discord)](https://discord.gg/z2YVKkycX3)
[![Ente's Blog RSS](https://img.shields.io/badge/blog-rss-F88900?style=for-the-badge&logo=rss&logoColor=white)](https://ente.io/blog/rss.xml)

View File

@@ -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",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -499,7 +499,6 @@
"appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。",
"duplicateCodes": "重複コード",
"noDuplicates": "✨ 重複なし",
"youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません",
"deduplicateCodes": "重複コード",
"deselectAll": "すべての選択を解除",
"selectAll": "すべて選択",

View File

@@ -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ą",

View File

@@ -1,7 +1,4 @@
{
"account": "അക്കൗണ്ട്",
"unlock": "അൺലോക്ക്",
"qrCode": "QR കോഡ്",
"blog": "ബ്ലോഗ്",
"verifyPassword": "പാസ്‌വേഡ് സ്ഥിരീകരിക്കുക",
"recreatePassword": "പാസ്‌വേഡ് പുനഃസൃഷ്ടിക്കുക",

View File

@@ -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."
}

View File

@@ -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",

View File

@@ -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(),
);
}

View File

@@ -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);

View File

@@ -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),
],
),
),
);
}
}

View File

@@ -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

View File

@@ -1,7 +1,6 @@
{
"tabWidth": 4,
"proseWrap": "always",
"objectWrap": "collapse",
"plugins": [
"prettier-plugin-organize-imports",
"prettier-plugin-packagejson"

View File

@@ -2,8 +2,6 @@
## v1.7.11 (Unreleased)
- Improved file viewer.
- Improved live photo experience.
- .
## v1.7.10

View File

@@ -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

View File

@@ -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,
},
],
},
},

View File

@@ -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"

View File

@@ -247,7 +247,12 @@ const registerPrivilegedSchemes = () => {
corsEnabled: true,
},
},
{ scheme: "stream", privileges: { supportFetchAPI: true } },
{
scheme: "stream",
privileges: {
supportFetchAPI: true,
},
},
]);
};

View File

@@ -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,
},
]);
};

View File

@@ -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,
});
}
}

View File

@@ -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`);

View File

@@ -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,
};
};
/**

View File

@@ -5,7 +5,9 @@ interface SafeStorageStore {
}
const safeStorageSchema: Schema<SafeStorageStore> = {
encryptionKey: { type: "string" },
encryptionKey: {
type: "string",
},
};
export const safeStorageStore = new Store({

View File

@@ -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({

View File

@@ -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.
*/

View File

@@ -34,7 +34,10 @@ const watchStoreSchema: Schema<WatchStore> = {
},
},
},
ignoredFiles: { type: "array", items: { type: "string" } },
ignoredFiles: {
type: "array",
items: { type: "string" },
},
},
},
},

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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 dont 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) wont automatically reflect in Ente. Youll 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.

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -54,9 +54,6 @@ The same principle applies if you're deploying to your custom domain.
## Replication
![Replication](/replication.png)
<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).

View File

@@ -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

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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"

View File

@@ -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"
// }
}
}

View File

@@ -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

View File

@@ -201,8 +201,8 @@ class SuperLogging {
}
unawaited(
getDeviceInfo().then((info) {
$.info("Device Info: $info");
getDeviceName().then((name) {
$.info("Device name: $name");
}),
);

View File

@@ -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';

View File

@@ -1,7 +0,0 @@
import "package:photos/events/event.dart";
class FileCaptionUpdatedEvent extends Event {
final int fileGeneratedID;
FileCaptionUpdatedEvent(this.fileGeneratedID);
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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);

View File

@@ -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!;
}

View File

@@ -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() {

View File

@@ -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,
});
}

View File

@@ -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), () {

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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>{};

View File

@@ -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,
);
}

View File

@@ -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,
});
}

View File

@@ -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()));

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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());
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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]);

View File

@@ -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();
},
),
),

View File

@@ -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;
}
}
}

View File

@@ -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,
),
),
],
),

View File

@@ -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();
}

View File

@@ -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(),
],
);
}
}

View File

@@ -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,
);
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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";
}

View File

@@ -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;
}
}

View 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,
),
),
);
}

View File

@@ -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:

View File

@@ -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

View 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.

View 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.

View 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

View File

@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

View File

@@ -0,0 +1 @@
/build

Some files were not shown because too many files have changed in this diff Show More