Compare commits

...

185 Commits

Author SHA1 Message Date
Jay
acf978c302 docs-ios 2025-03-12 12:17:47 +05:30
Neeraj
26c35d997a [mob] Reduce fully gallery reload during upload matching (#5283)
## Description

## Tests
2025-03-12 11:56:06 +05:30
Manav Rathi
85d6552943 [web] New translations (#5290)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2025-03-12 08:39:01 +05:30
Crowdin Bot
56876e4a28 New Crowdin translations by GitHub Action 2025-03-12 03:01:31 +00:00
Manav Rathi
738128a7c5 [web] File viewer translation related cleanup (#5289) 2025-03-12 08:30:38 +05:30
Manav Rathi
eab1d98d2d tr 2025-03-12 08:24:05 +05:30
Manav Rathi
7aa4f5bb16 l 2025-03-12 08:15:08 +05:30
Manav Rathi
27fa57f608 More 2025-03-12 08:05:22 +05:30
Manav Rathi
f135b65d31 sc 2025-03-12 08:02:59 +05:30
Manav Rathi
9b09ebe3f0 fav 2025-03-12 07:58:26 +05:30
Vishnu Mohandas
9e64752677 [auth] Add icon for LinkedIn (#5284)
## Description

Add SVG icon for LinkedIn: https://www.linkedin.com/.

See logo being used here:
https://en.m.wikipedia.org/wiki/File:LinkedIn_icon.svg
2025-03-12 07:55:49 +05:30
Manav Rathi
563d65cc1d tr 2025-03-12 07:09:58 +05:30
Manav Rathi
68132147e7 Reuse 2025-03-12 06:53:50 +05:30
Manav Rathi
6979617d12 Prune 2025-03-12 06:46:07 +05:30
Manav Rathi
c4812abab3 Use 2025-03-12 06:36:48 +05:30
Manav Rathi
f09ef7ae10 pn 2025-03-12 06:35:56 +05:30
Manav Rathi
062bbdfa88 list join 2025-03-12 06:33:04 +05:30
Daniel Tsiang
1f87ef8cb7 [auth] Add icon for LinkedIn 2025-03-11 15:47:15 +00:00
Neeraj
df14f18881 [mob][perf] Improve computation for discover sections by batching lookup (#5282)
## Description
- For 14k files, this reduced the overall time to calculate discovery
section from **1600ms** to **250ms**
- This should also reduce the memory overhead as we have reduce the
number of times we are passing vector to different isolate.

## Tests
2025-03-11 17:43:37 +05:30
Neeraj Gupta
c4d8ddbf26 [mob] add docs 2025-03-11 17:32:50 +05:30
Neeraj Gupta
9132be591d [mob][perf] Avoid db reload on mapping local file to remote file 2025-03-11 17:30:00 +05:30
Neeraj Gupta
2fa555163c [mob] Batch query lookup for discover sections 2025-03-11 16:48:54 +05:30
Neeraj Gupta
643da1491a [mob] Add support for getting clip results for multiple queries 2025-03-11 15:07:31 +05:30
Neeraj
3b568bf914 [mob] Refactor local import (#5280)
## Description

## Tests
2025-03-11 12:32:44 +05:30
mangesh
fd325d0be5 [doc] family limits docs (#5268) 2025-03-11 11:39:59 +05:30
Neeraj
8e158677f2 Update family-plans.md 2025-03-11 11:39:44 +05:30
Sven
d8490ea4b1 [auth] Add 2 new icons (MEXC & ICONOMI) (#5271)
Add icons for MEXC and ICONOMI to Ente Auth
2025-03-11 11:38:41 +05:30
Ashil
01e258557c [mob][photo] Show file caption/description in file viewer. (#5279)
## Description

- Tapping on description/caption will open file info.

<img
src="https://github.com/user-attachments/assets/0f9422ec-49bb-43d8-9568-b57748587866"
width="300px">

<img
src="https://github.com/user-attachments/assets/43b704b4-6fc4-44ed-8d7a-97b7d27c90b0"
width="300px">

<img
src="https://github.com/user-attachments/assets/65fca334-14a7-4f01-95c4-46b231687438"
width="300px">

<img
src="https://github.com/user-attachments/assets/8e56cb29-7af6-439e-8627-3badc60aa383"
width="300px">
2025-03-11 11:36:11 +05:30
Bl4ckspell
f9dbf0efea [auth] add luma icon (#5276)
## Description

![Luma](https://github.com/user-attachments/assets/90a404bf-0302-40e9-9653-900dfbfc3a6c)

## Tests
2025-03-11 11:35:37 +05:30
ashilkn
51ef7c60fa [mob][photos] Fix render overlow 2025-03-11 11:21:55 +05:30
Neeraj
600736e70f [auth] Add support for editing number of digits & algorithm (#5190)
## Description
This PR add support to edit the number of digits (between 1 to 10) for
the 2FA codes and also give an option to select algorithms

![image](https://github.com/user-attachments/assets/be4b8c01-0d94-4881-b23d-32e03c14dbeb)
2025-03-11 10:13:31 +05:30
Manav Rathi
ef3ccbd91b zoom 2025-03-11 09:58:50 +05:30
Manav Rathi
55a68f9d29 tr 2025-03-11 09:54:51 +05:30
Manav Rathi
5918698366 reuse 2025-03-11 09:36:56 +05:30
Manav Rathi
d0b58b75c8 Tweak 2025-03-11 09:32:03 +05:30
Manav Rathi
a72eb78e53 Center 2025-03-11 09:29:18 +05:30
Manav Rathi
8d07b16e09 [web] Misc minor fixes (#5278) 2025-03-11 09:09:18 +05:30
Manav Rathi
caadba3996 Fix empty space
This had been there earlier, had accidentally gotten removed during search bar refactoring
2025-03-11 09:00:24 +05:30
Manav Rathi
427cc9d414 Fix 2025-03-11 08:13:42 +05:30
Manav Rathi
d8995ef375 [web] File viewer code cleanup (#5275) 2025-03-10 21:18:58 +05:30
Manav Rathi
c3831230e0 Move 2025-03-10 21:10:39 +05:30
Manav Rathi
76d8038899 Let PhotoSwipe show the error 2025-03-10 20:30:45 +05:30
Manav Rathi
ad0169b7e5 cc 2025-03-10 19:56:46 +05:30
Manav Rathi
76887b2205 shared shortcuts 2025-03-10 19:50:39 +05:30
Manav Rathi
7249b25180 F 2025-03-10 19:44:49 +05:30
Manav Rathi
ffd2a55ca0 Retain previous (pre-ps5) behaviour 2025-03-10 19:27:55 +05:30
Manav Rathi
eaf576967b Chrome warnings 2025-03-10 19:24:18 +05:30
Manav Rathi
e6a9ccefe7 ts 2025-03-10 19:01:54 +05:30
Manav Rathi
d0b25b31c8 ts 2025-03-10 18:42:38 +05:30
Manav Rathi
3211e6afe6 Remove the auto hide code, it is too distracting to enable 2025-03-10 18:19:49 +05:30
Manav Rathi
a7cc5e7165 ts 2025-03-10 18:05:43 +05:30
Manav Rathi
bea32ac7e3 Re 2025-03-10 17:56:10 +05:30
Manav Rathi
3be7f7b55e Inline 2025-03-10 17:47:56 +05:30
Manav Rathi
4a833e0799 Final two 2025-03-10 17:47:56 +05:30
Manav Rathi
10a9ad02f8 Remove no longer needed zi workarounds 2025-03-10 17:47:56 +05:30
ashilkn
ba79588090 [mob][photos] Fix text colour 2025-03-10 17:38:22 +05:30
ashilkn
3593a8e545 [mob][photos] Open file info bottom sheet when tapped on file description/caption 2025-03-10 17:29:02 +05:30
Manav Rathi
643a6cf413 Trim 2025-03-10 16:38:46 +05:30
ashilkn
dbb14f0a24 [mob][photos] Reflect edited caption/description immidiately on file viewer on changing it in file info bottom sheet 2025-03-10 16:36:31 +05:30
Manav Rathi
cba6676bb5 Empty state 2025-03-10 16:26:25 +05:30
Manav Rathi
d43cf1fb86 Fin annotation propagation 2025-03-10 15:54:12 +05:30
Manav Rathi
f02974045b Move 2025-03-10 15:53:54 +05:30
Manav Rathi
20268c236a CL 2025-03-10 15:53:54 +05:30
Manav Rathi
0b7aa97db1 wip re 2025-03-10 15:17:58 +05:30
Manav Rathi
9a39298acd Re 2025-03-10 15:17:58 +05:30
Manav Rathi
36e1e758c5 Re 2025-03-10 15:17:58 +05:30
Manav Rathi
f74f13c7a8 web doesn't need the submodule fetch anymore 2025-03-10 15:17:57 +05:30
Vishnu Mohandas
eb9e61579e [docs] Update README.md (#5270)
Fixes https://github.com/ente-io/ente/issues/5262.
2025-03-10 13:34:22 +05:30
Vishnu Mohandas
300b3c89a3 Update README.md 2025-03-10 13:33:52 +05:30
Laurens Priem
302d2af3d2 ;[mob][photos] Memories iteration for internal users (#5253)
## Description

Some minor iterations:
- Added base locations to location section
- More debugging options in moments section
- Performance logging
- Minor tweaks 

## Tests

Tested in debug mode on my pixel phone.
2025-03-10 13:33:51 +05:30
laurenspriem
3feee66d3a Merge branch 'main' into memories_iteration 2025-03-10 13:31:54 +05:30
ashilkn
b953d6d513 [mob][photos] Clean up 2025-03-10 13:27:04 +05:30
laurenspriem
d88b39ec46 [mob][photos] bump for internal release 2025-03-10 13:26:11 +05:30
ashilkn
145e025eea [mob][photos] Move caption/description inside seek bar's container in media kit player for consistancy of UI across players 2025-03-10 13:22:56 +05:30
ashilkn
13c36d9c40 [mob][photos] Hide/show caption with enabling/disabling full screen 2025-03-10 13:13:52 +05:30
github-actions[bot]
dd807368b2 [auth] New translations (#5266)
New translations from
[Crowdin](https://crowdin.com/project/ente-authenticator-app)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-10 12:41:36 +05:30
Iiii-I-I-I
0f0790df5f [Auth] Add two custom icons (#5267)
## Description
Add icons for:

* Federal Student Aid ([studentaid.gov](https://studentaid.gov)), rank
557 on [Similarweb](https://www.similarweb.com/website/studentaid.gov/)
* RuneScape Wiki ([runescape.wiki](https://runescape.wiki/)), rank 2,652
on [Similarweb](https://www.similarweb.com/website/runescape.wiki/)
2025-03-10 12:24:38 +05:30
mangesh
9d2e1cd502 [server] Soft error msgs when billing/payment data(config) is not found (#5269)
From discord discussion

the pr makes changes to the error strings which are thrown when
configuration reltaed to payment and billing isn't found. The error
messages are changed so to not scare self hosters while we are aware of
it on the production instance.
2025-03-10 12:24:21 +05:30
mngshm
a640a430bf tweak 2025-03-10 12:08:08 +05:30
mngshm
26cb6ad722 [server] avoid scary error msgs if payment/billing configuration is not found 2025-03-10 11:58:59 +05:30
mngshm
b114dd54b9 [doc] family limits docs 2025-03-10 11:28:29 +05:30
ashilkn
b8e3d88575 [mob][photos] Show caption/description in file viewer screen 2025-03-10 11:22:50 +05:30
mangesh
b3d5731731 [docs] replication diagram by @maazy4ever (#5245) 2025-03-10 11:08:34 +05:30
Manav Rathi
84b880d7cf [web] Doesn't require submodules anymore (#5258) 2025-03-07 20:33:53 +05:30
Manav Rathi
3900ee609f Fix clicks on hidden buttons 2025-03-07 20:18:52 +05:30
Manav Rathi
0ee496401a Mention ps 2025-03-07 19:52:23 +05:30
Manav Rathi
511c324bad [web] Submodules required no more 2025-03-07 19:50:06 +05:30
Manav Rathi
51f2868f98 [web] Handle deletion of last slide (#5257)
...in the new file viewer.
2025-03-07 19:45:08 +05:30
Manav Rathi
2a70327153 [web] Handle deletion of last slide 2025-03-07 19:40:11 +05:30
Manav Rathi
f29341ccb2 [web] Ask prettier to not modify objectWrap in translations (#5256)
The crowdin action reverts this otherwise
2025-03-07 19:27:21 +05:30
Manav Rathi
89b35f44c3 Not needed anymore 2025-03-07 19:22:11 +05:30
Manav Rathi
beeafe4aa6 The crowdin action reverts this otherwise 2025-03-07 19:22:11 +05:30
Manav Rathi
4b631aa423 [web] New translations (#5255)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-web)
2025-03-07 19:16:08 +05:30
Crowdin Bot
83729aced4 New Crowdin translations by GitHub Action 2025-03-07 13:44:23 +00:00
Manav Rathi
bbcfa865d1 [web] Dependency updates + prettier's new objectWrap=collapse (#5254) 2025-03-07 19:13:03 +05:30
Manav Rathi
4bf629a44c yarn prettier --write --object-wrap=collapse 2025-03-07 18:54:04 +05:30
Manav Rathi
4e417e9490 LF 2025-03-07 18:44:45 +05:30
Manav Rathi
b6aefd1845 [web] Dependency updates 2025-03-07 18:21:05 +05:30
laurenspriem
a68f1e91c5 [mob][photos] Performance logging 2025-03-07 18:18:58 +05:30
Manav Rathi
a3e8d3c1a3 yarn prettier --write --object-wrap=collapse . 2025-03-07 17:53:58 +05:30
Manav Rathi
15473d80d8 Run linter 2025-03-07 17:47:57 +05:30
Manav Rathi
fa349caf0c [desktop] Dep updates 2025-03-07 17:36:05 +05:30
laurenspriem
920e26255c [mob][photos] Surface calculated persons 2025-03-07 16:49:47 +05:30
Manav Rathi
2a3466da63 [web] Modify the cursor on file viewer thumbnail (#5252) 2025-03-07 16:15:11 +05:30
Manav Rathi
c59da52f71 Modify the cursor on thumbnail 2025-03-07 16:08:34 +05:30
Manav Rathi
aa551463b3 Prefix 2025-03-07 14:38:06 +05:30
laurenspriem
949909631a [mob][photos] don't sort debug memories 2025-03-07 14:09:01 +05:30
laurenspriem
de2b399941 [mob][photos] datepicker for debugging memories 2025-03-07 14:06:58 +05:30
laurenspriem
6685c68c35 [mob][photos] Show ALL memories in moments section [debug] 2025-03-07 13:03:50 +05:30
Manav Rathi
02f1ac4f2f [web] Update PhotoSwipe (Complete) (#5249)
- Swaps our forked version of PhotoSwipe with the latest upstream.
- Many(!) improvements to the file viewer at the same time.

There is a further bunch of cleanup, but that can be done async later.
2025-03-07 11:31:53 +05:30
Manav Rathi
573cc787e5 Prune 2025-03-07 11:24:10 +05:30
Manav Rathi
997b87bd26 Swap fin 2025-03-07 11:15:40 +05:30
Manav Rathi
ef013473fc Swap 2025-03-07 11:07:46 +05:30
Manav Rathi
df96f42a61 Install 2025-03-07 10:35:06 +05:30
Manav Rathi
a144d39a47 Styles 2025-03-07 10:32:21 +05:30
Manav Rathi
70c98b8877 Remove the submodule version of photoswipe 2025-03-07 10:28:42 +05:30
Manav Rathi
2a5f774423 [desktop] Electron minor version update (#5248) 2025-03-07 10:27:19 +05:30
Manav Rathi
4796d8a54a [desktop] Electron minor version update 2025-03-07 10:24:23 +05:30
Manav Rathi
694a8a46dd [web] PhotoSwipe Update - Before switch over (#5247)
Final set of changes, in next PR we swap
2025-03-07 10:20:48 +05:30
Manav Rathi
61809889e9 Revert "Temporary workbench"
This reverts commit 3bb92e10e4.
2025-03-07 10:16:04 +05:30
Manav Rathi
981716fbcb vid shortcuts 2025-03-07 08:59:15 +05:30
Manav Rathi
be25081a73 Loader 2025-03-07 08:48:12 +05:30
Manav Rathi
8e3e741b1a Flip 2025-03-07 08:40:37 +05:30
Manav Rathi
a056cfd154 Start dusting 2025-03-07 07:52:00 +05:30
Manav Rathi
98987326e2 ditto 2025-03-07 07:45:57 +05:30
Manav Rathi
b9de012c28 Better counter behaviour on moving into two lines 2025-03-07 07:15:45 +05:30
Manav Rathi
50adfa7399 Fix error position 2025-03-07 07:04:21 +05:30
Manav Rathi
2d005a7d07 For future us 2025-03-07 06:58:28 +05:30
Manav Rathi
4faf938fbd fav cleanup 2025-03-07 06:57:16 +05:30
Manav Rathi
3bb92e10e4 Temporary workbench
This reverts commit 1eed87e117.
2025-03-07 06:57:16 +05:30
Ashil
9f51c2ddae [mob][photos] Log android version along with device name (#5240)
### Description 

Logging the Android version will make it easier to identify if an issue
is linked to certain Android version(s).
2025-03-07 05:21:41 +05:30
mngshm
2a453ee321 replication diagram by @maazy4ever 2025-03-06 22:04:31 +05:30
Manav Rathi
a48505205e [web] PhotoSwipe update - WIP (#5244) 2025-03-06 20:24:15 +05:30
Manav Rathi
6697cca571 Revert "Temporary workbench"
This reverts commit ae4e189848.
2025-03-06 20:16:47 +05:30
Manav Rathi
bfc0f785bc Top bar mobile 2025-03-06 20:16:47 +05:30
Manav Rathi
f3cc4f6fa0 lp fix if nearby slide 2025-03-06 19:42:44 +05:30
Manav Rathi
781de2b60b Single element 1 2025-03-06 19:37:05 +05:30
Aman Raj Singh Mourya
dfe892b54e [auth] Minor fix 2025-03-06 19:33:48 +05:30
Manav Rathi
fce9c6d01e Focus handle 2025-03-06 19:16:32 +05:30
Manav Rathi
183000526c sp 2025-03-06 19:06:45 +05:30
Manav Rathi
0b50d43d53 Fix vis 2025-03-06 18:47:59 +05:30
Manav Rathi
f48d97112c toggle 2 2025-03-06 18:13:26 +05:30
Manav Rathi
815009da9b across slides 2025-03-06 18:13:26 +05:30
Manav Rathi
e9e0b31b8a Tweak 2025-03-06 18:13:26 +05:30
Manav Rathi
b72f65d44c play 2 2025-03-06 18:13:26 +05:30
Manav Rathi
5649ee7c03 live 1 2025-03-06 18:13:26 +05:30
Manav Rathi
74f301e936 Tweak 2025-03-06 18:13:26 +05:30
Manav Rathi
03df527fb7 live 1 2025-03-06 18:13:26 +05:30
Manav Rathi
6c9887613b mark pending 2025-03-06 18:13:26 +05:30
Manav Rathi
ca7ee5e147 Consistent auto hide with slide changes 2025-03-06 18:13:26 +05:30
Manav Rathi
c8dc9c9f46 ks 2025-03-06 18:13:26 +05:30
Manav Rathi
7eaedfe138 Fixes 2025-03-06 18:13:26 +05:30
Manav Rathi
30b23e6c3b Use closures consistently 2025-03-06 18:13:26 +05:30
Manav Rathi
b578c8f0de help 2025-03-06 18:13:26 +05:30
Manav Rathi
ba95d08cdd kbd fin 2025-03-06 18:13:26 +05:30
Manav Rathi
63faa29cd4 occam 2025-03-06 18:13:25 +05:30
Manav Rathi
27ad9840d0 Reroute so that it works with kbd shortcuts 2025-03-06 18:13:25 +05:30
Manav Rathi
c96f2495ed pseudo focus 2025-03-06 18:13:25 +05:30
Manav Rathi
b1c680cccd wasd fix 2025-03-06 18:13:25 +05:30
Manav Rathi
f487e64569 wasd 2025-03-06 18:13:25 +05:30
Manav Rathi
ae4e189848 Temporary workbench
This reverts commit 1eed87e117.
2025-03-06 18:13:25 +05:30
Manav Rathi
5ab8169cd9 [desktop] Passthrough unknown entity data fields (#5241) 2025-03-06 18:04:57 +05:30
Manav Rathi
f52b6256b5 Update 2025-03-06 18:01:24 +05:30
Manav Rathi
c03f63d2b2 [desktop] Passthrough unknown entity data fields 2025-03-06 17:51:37 +05:30
laurenspriem
e2aea63276 [mob][photos] base locations in locations section 2025-03-06 16:33:41 +05:30
ashilkn
f590a43159 [mob][photos] Log android version along with device name 2025-03-06 16:15:15 +05:30
Neeraj
bc72ec1982 [mob] Refactor permission related logic (#5239)
## Description

## Tests
2025-03-06 16:14:10 +05:30
Neeraj Gupta
7050ba5f22 [mob] Lint fix 2025-03-06 16:07:47 +05:30
Neeraj Gupta
2e2cc7f3e7 Merge remote-tracking branch 'origin/main' into refactor_perm 2025-03-06 15:43:55 +05:30
Neeraj Gupta
2278b1f40e [mob] Refactor 2025-03-06 15:41:37 +05:30
Neeraj Gupta
69852e436a refactor 2025-03-06 14:06:42 +05:30
Neeraj Gupta
3fe47dd4c4 [mob] Add permission service 2025-03-06 13:13:12 +05:30
laurenspriem
5ff494320c [mob][photos] trips dont repeat early 2025-03-05 17:43:51 +05:30
laurenspriem
d49f9cc054 [mob][photos] Use constants 2025-03-05 17:12:46 +05:30
laurenspriem
c432125113 [mob][photos] Make base locations more robust 2025-03-05 16:41:12 +05:30
laurenspriem
d25e81282d [mob][photos] Mini refactor 2025-03-05 13:13:22 +05:30
laurenspriem
2d30ac4c46 [mob][photos] include import 2025-03-05 12:43:40 +05:30
laurenspriem
49fe5f41e0 [mob][photos] easier debugging 2025-03-05 12:42:49 +05:30
Aman Raj Singh Mourya
2aa953d5b6 [auth] Minor fixes 2025-03-03 12:26:54 +05:30
Aman Raj Singh Mourya
b35cd47c8a [auth] Show advance option only when code setup 2025-02-27 20:35:52 +05:30
Aman Raj Singh Mourya
24759a3923 [auth] Refactoring 2025-02-26 23:44:30 +05:30
Aman Raj Singh Mourya
1fba250f74 [auth] Remove log statement 2025-02-26 23:39:04 +05:30
Aman Raj Singh Mourya
8099cbd990 [auth] Minor fixes 2025-02-26 23:32:24 +05:30
Aman Raj Singh Mourya
b1ed3a6302 [auth] Add UI to select algorithm 2025-02-26 23:31:58 +05:30
Aman Raj Singh Mourya
3a955f2b04 [auth] Add support for editing number of digits & algorithm type 2025-02-26 23:31:16 +05:30
274 changed files with 5184 additions and 5943 deletions

View File

@@ -26,8 +26,6 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4

View File

@@ -26,8 +26,6 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4

View File

@@ -34,7 +34,6 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ steps.select-branch.outputs.branch }}
submodules: recursive
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4

View File

@@ -30,8 +30,6 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup node and enable yarn caching
uses: actions/setup-node@v4

4
.gitmodules vendored
View File

@@ -9,7 +9,3 @@
[submodule "auth/assets/simple-icons"]
path = auth/assets/simple-icons
url = https://github.com/simple-icons/simple-icons.git
[submodule "web/apps/photos/thirdparty/photoswipe"]
path = web/apps/photos/thirdparty/photoswipe
url = https://github.com/ente-io/PhotoSwipe.git
branch = single-thread

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 our [community page](https://ente.io/community) for all the ways to
connect with the community.
Please visit the [community section](https://ente.io/about#community) for all the ways to
connect with our community.
[![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,6 +379,14 @@
{
"title": "Fastmail"
},
{
"title": "Federal Student Aid",
"slug": "federal_student_aid",
"altNames": [
"FSA",
"FAFSA"
]
},
{
"title": "Fidelity",
"slug": "fidelity",
@@ -483,6 +491,9 @@
"title": "IceDrive",
"slug": "ice_drive"
},
{
"title": "ICONOMI"
},
{
"title": "ID.me",
"slug": "id_me"
@@ -593,6 +604,11 @@
{
"title": "Letterboxd"
},
{
"title": "LinkedIn",
"slug": "linkedin",
"hex": "2596be"
},
{
"title": "Linux.Do",
"slug": "linux_do",
@@ -614,6 +630,14 @@
"title": "Login.gov",
"slug": "login_gov"
},
{
"title": "Luma",
"slug": "luma",
"altNames": [
"luma",
"lu.ma"
]
},
{
"title": "Marketplace.tf",
"slug": "marketplacedottf"
@@ -643,6 +667,9 @@
"MercadoLivre"
]
},
{
"title": "MEXC"
},
{
"title": "microsoft"
},
@@ -952,6 +979,10 @@
{
"title": "RuneMate"
},
{
"title": "RuneScape Wiki",
"slug": "runescape_wiki"
},
{
"title": "Rust Language Forum",
"slug": "rust_language_forum",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 35 30" version="1.1" style="zoom: 16;" visibility="visible"><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" visibility="visible"><g id="Group-8"><g id="Group-7" fill="#3A79F2"><rect id="Rectangle-Copy-20" x="9" y="0" width="6" height="30" rx="3"></rect><rect id="Rectangle-Copy-21" x="27" y="12" width="6" height="9" rx="3"></rect><rect id="Rectangle-Copy-22" x="18" y="12" width="6" height="18" rx="3" visibility="visible"></rect><rect id="Rectangle-Copy-23" x="0" y="21" width="6" height="9" rx="3" visibility="visible"></rect><circle id="Oval-Copy-13" cx="21" cy="6" r="3" visibility="visible"></circle></g></g></g></svg>

After

Width:  |  Height:  |  Size: 750 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="72" viewBox="0 0 72 72" width="72" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z" fill="#007EBB"/><path d="M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
<path
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns:xodm="http://www.corel.com/coreldraw/odm/2003" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 450 2500 1650" style="enable-background:new 0 0 2500 2500;" xml:space="preserve">
<rect y="250" width="2500" height="1650" style="fill:none;"></rect>
<g id="_2500406570000">
<path d="M2459.7,1566.6l-540.6-937.7c-118.5-195.5-407.5-197.5-521.9,8.3l-567.6,975.2c-106,178.8,25,403.3,237.1,403.3H2204C2418.1,2015.7,2578.2,1784.9,2459.7,1566.6z" style="fill:#3156AA;"></path>
<path d="M1680,1639.4l-33.3-58.2c-31.2-54.1-99.8-170.5-99.8-170.5l-457.4-794.3C971,439.7,690.3,425.1,571.8,647.6L39.5,1568.7c-110.2,193.4,20.8,444.9,259.9,447h1131.1h482.4h286.9C1906.7,2017.8,1813.1,1866,1680,1639.4L1680,1639.4z" style="fill:#1972E2;"></path>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="703.637" y1="1211.6566" x2="1935.647" y2="727.2267" gradientTransform="matrix(1 0 0 -1 0 2497.8899)">
<stop offset="0" style="stop-color:#264CA2;stop-opacity:0;"></stop>
<stop offset="1" style="stop-color:#234588;"></stop>
</linearGradient>
<path d="M1680.1,1639.4l-33.3-58.2c-31.2-54.1-99.8-170.5-99.8-170.5l-295.3-519.8l-424.2,723.6c-106,178.8,25,403.4,237,403.4h363.9h482.4h289C1904.6,2015.7,1813.1,1866,1680.1,1639.4L1680.1,1639.4z" style="fill:url(#SVGID_1_);"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 35 35">
<g fill="#438ab5" fill-rule="evenodd" transform="translate(4 1)">
<path d="M11.7311354 23.0557769L9.91249858 23.0557769 8.79846449 26.6069057 6.97030597 23 6.79891611 23 4.98027925 26.6347942 3.85672349 23.0557769 2 23.0557769 4.54228294 29.9814077 4.75175943 30 6.86556772 25.9189907 9.00794098 30 9.22693914 29.9814077 11.7311354 23.0557769zM14.3322795 29.8698539L14.3322795 23.0557769 12.7135975 23.0557769 12.7135975 29.8698539 14.3322795 29.8698539zM22.2084227 29.8698539L19.1900568 26.3001328 22.0560762 23.0557769 20.1422227 23.0557769 17.6951564 26.0212483 17.6951564 23.0557769 16.0764744 23.0557769 16.0764744 29.8698539 17.6951564 29.8698539 17.6951564 26.6812749 20.2564826 29.8698539 22.2084227 29.8698539zM25 29.8698539L25 23.0557769 23.381318 23.0557769 23.381318 29.8698539 25 29.8698539zM24.4742178 8.98009586L24.4742178 5.59616787C24.4732494 5.5136357 24.4163953 5.44228159 24.3362569 5.42252065 23.0272354 5.11977548 21.7162784 4.97854841 20.4033857 4.99883944 18.3648587 5.0303453 15.6405277 6.73541461 15.8150159 9.26543298 15.9313414 10.9521119 16.9379659 12.3146739 18.8348893 13.3531189 21.1050121 14.6587079 22.1112168 16.0505228 21.8535034 17.5285637 21.4669332 19.7456249 19.4833026 20.2699349 18.2011186 20.9636596 19.8933668 21.0568854 21.1108284 20.9541788 21.8535034 20.6555398 23.5576643 19.970275 24.621281 18.4776117 24.8765595 17.2814785 25.5814 13.9788769 23.0921699 12.4640398 21.8535034 11.6272857 20.6148368 10.7905315 18.5555838 9.39712448 18.5555838 8.2423436 18.5555838 7.08756273 19.0354769 6.19945178 20.606059 5.98878728 22.2560942 5.76746561 23.8084838 6.80552306 24.0666162 8.65926511 24.1000214 8.89915966 24.2358886 9.00610324 24.4742178 8.98009586z"/>
<path d="M12.1896778,5.73473633 C12.2458703,5.76929923 12.2836806,5.79287044 12.3031088,5.80544997 C13.8305405,6.79444234 14.5459886,7.96859313 14.4494531,9.32790236 C14.3458984,10.7860487 13.4278718,12.1833682 11.6953731,13.5198609 C11.995423,13.6024263 13.0716006,15.2517434 14.923906,18.4678119 C15.9400176,19.5870375 17.2645126,20.0440386 18.8973912,19.8388151 C17.7166822,20.6938532 16.5941307,21.0918329 15.5297368,21.032754 C13.9331458,20.9441357 12.5153495,20.0153267 11.6953731,18.9752651 C10.8753968,17.9352035 9.17647457,14.3916396 8.02078511,13.3656207 C9.24887971,13.3176267 10.0712516,13.0717507 10.4879009,12.6279929 C11.0163711,12.0651387 11.4324817,11.1727564 11.3052905,9.86386602 C11.242381,9.21648063 10.8576813,8.46000935 10.2600254,7.66096138 C10.1677374,7.53757512 10.1984144,7.42387917 10.3520565,7.31987355 C10.8366434,7.01067102 11.3224095,6.50739801 11.8093549,5.81005452 L11.8102004,5.81066099 C11.8971472,5.68944809 12.0629706,5.65600737 12.1900999,5.73404867 Z"/>
<path d="M5.46922112,0 C5.93751334,0 6.45488645,0.251926659 6.49405028,0.821037745 C6.52015951,1.20044514 6.40971704,1.46961432 6.16272288,1.62854529 L6.36548563,4.50160863 L9.73880697,4.59010439 C9.80515586,4.59184498 9.86880672,4.61673544 9.91873596,4.66046503 L10.8936611,5.51433494 C11.0118247,5.61782632 11.0237189,5.79751318 10.9202275,5.91567678 C10.9171724,5.91916507 10.9140324,5.92257811 10.9108103,5.92591286 L10.3160188,6.541511 C10.2118589,6.64931459 10.0419078,6.65776756 9.92756462,6.56083181 L9.4018415,6.11514401 L9.4018415,6.11514401 L7.33749093,6.11514401 C6.98986751,6.27375711 6.78712075,6.48688034 6.72925065,6.75451369 C6.67138054,7.02214704 6.66841118,9.38843602 6.72034254,13.8533806 C6.72034254,15.5011837 6.88214839,17.3116009 7.20576008,19.2846324 L5.58460752,21.9888272 L3.70958016,19.2846324 C4.08537518,17.1566151 4.27327269,15.282922 4.27327269,13.6635531 L3.43377358,12.9035744 L4.28218079,12.0252455 C4.31100967,8.71955904 4.31100967,6.96264844 4.28218079,6.75451369 C4.23893746,6.44231156 4.03865152,6.30830705 3.71848826,6.11514401 L1.69132923,6.11514401 L1.15474102,6.5615377 C1.03891569,6.65789407 0.868043682,6.64720638 0.765127903,6.53716821 L0.191996049,5.92437216 C0.0855950374,5.81060756 0.0905023165,5.63241981 0.203003442,5.52468375 L1.09677655,4.66876709 C1.14782548,4.61988037 1.2152487,4.59175365 1.28590527,4.58986886 L4.5946007,4.50160863 L4.5946007,4.50160863 L4.76223107,1.62854529 C4.55067524,1.43081789 4.44489732,1.16164871 4.44489732,0.821037745 C4.44489732,0.310121294 5.0009289,0 5.46922112,0 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -499,6 +499,7 @@
"appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.",
"duplicateCodes": "Duplicar códigos",
"noDuplicates": "✨ No hay duplicados",
"youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar",
"deduplicateCodes": "Desduplicar códigos",
"deselectAll": "Deseleccionar todo",
"selectAll": "Seleccionar todo",
@@ -509,6 +510,7 @@
"supportEnte": "Apoya a <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Danos una estrella en GitHub",
"free5GB": "5 GB gratis en <bold-green>ente</bold-green> Fotos",
"loginWithAuthAccount": "Inicia sesión con tu cuenta de Auth",
"freeStorageOffer": "10% de descuento en <bold-green>ente</bold-green> fotos",
"freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año"
}

View File

@@ -499,6 +499,7 @@
"appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.",
"duplicateCodes": "Codici duplicati",
"noDuplicates": "✨ Nessun doppione",
"youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati",
"deduplicateCodes": "Codici deduplicati",
"deselectAll": "Deselezionare tutti",
"selectAll": "Seleziona tutti",

View File

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

View File

@@ -499,6 +499,7 @@
"appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.",
"duplicateCodes": "Dubliuoti kodus",
"noDuplicates": "✨ Dublikatų nėra",
"youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.",
"deduplicateCodes": "Atdubliuoti kodus",
"deselectAll": "Naikinti visų pasirinkimą",
"selectAll": "Pasirinkti viską",

View File

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

View File

@@ -88,6 +88,8 @@
"useRecoveryKey": "Uporabi ključ za obnovo",
"incorrectPasswordTitle": "Nepravilno geslo",
"welcomeBack": "Dobrodošli nazaj!",
"emailAlreadyRegistered": "E-poštni naslov je že registriran.",
"emailNotRegistered": "E-poštni naslov ni registriran.",
"madeWithLoveAtPrefix": "ustvarjeno s ❤pri ",
"supportDevs": "Naročite se na <bold-green>ente</bold-green>, da nas podprete",
"supportDiscount": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto",
@@ -156,6 +158,7 @@
"twoFactorAuthTitle": "Dvojno preverjanja pristnosti",
"passkeyAuthTitle": "Potrditev ključa za dostop (passkey)",
"verifyPasskey": "Potrdite ključ za dostop (passkey)",
"loginWithTOTP": "Prijava z TOTP",
"recoverAccount": "Obnovi račun",
"enterRecoveryKeyHint": "Vnesite vaš ključ za obnovitev",
"recover": "Obnovi",
@@ -257,6 +260,10 @@
"areYouSureYouWantToLogout": "Ali ste prepričani, da se želite odjaviti?",
"yesLogout": "Ja, odjavi se",
"exit": "Izhod",
"theme": "Tema",
"lightTheme": "Svetla",
"darkTheme": "Temna",
"systemTheme": "Sistemska",
"verifyingRecoveryKey": "Preverjanje ključa za obnovitev",
"recoveryKeyVerified": "Ključ za obnovitev preverjen",
"recoveryKeySuccessBody": "Odlično! Vaš ključ za obnovitev je veljaven. Hvala za preverjanje.\n\nNe pozabite shraniti varnostno kopijo obnovitvenega ključa.",
@@ -327,6 +334,8 @@
}
}
},
"manualSort": "Po meri",
"editOrder": "Uredi vrstni red",
"mostFrequentlyUsed": "Pogosto uporabljeni",
"mostRecentlyUsed": "Nedavno uporabljeno",
"activeSessions": "Aktivne seje",
@@ -448,6 +457,8 @@
"customEndpoint": "Povezano na {endpoint}",
"pinText": "Pripni",
"unpinText": "Odpni",
"pinnedCodeMessage": "{code} je bila pripeta",
"unpinnedCodeMessage": "{code} je bila odpeta",
"pinned": "Pripeto",
"tags": "Oznake",
"createNewTag": "Ustvari novo oznako",
@@ -485,5 +496,21 @@
"appLockNotEnabled": "Zaklepanje aplikacije ni omogočeno",
"appLockNotEnabledDescription": "Prosimo, omogočite zaklepanje aplikacije v Nastavitve > Zaklepanje Aplikacije (Security > App Lock)",
"authToViewPasskey": "Da vidite passkey, se overite",
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen."
"appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen.",
"duplicateCodes": "Podvojene kode",
"noDuplicates": "✨ Ni duplikatov",
"youveNoDuplicateCodesThatCanBeCleared": "Nimate nobenih podvojenih kod, ki bi jih bilo mogoče izbrisati",
"deduplicateCodes": "Dedupliciraj kode",
"deselectAll": "Prekliči celoten izbor",
"selectAll": "Izberi vse",
"deleteDuplicates": "Izbriši dvojnike",
"plainHTML": "Navadni HTML",
"tellUsWhatYouThink": "Povejte nam kaj mislite",
"dropReview": "Napišite oceno v trgovini App/Play Store",
"supportEnte": "Podpiraj <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Dajte nam zvezdico na Githubu",
"free5GB": "5 GB zastonj na <bold-green>ente</bold-green> fotografije",
"loginWithAuthAccount": "Prijavite se s svojim Auth računom",
"freeStorageOffer": "10 % popust na <bold-green>ente</bold-green> fotografije",
"freeStorageOfferDescription": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto"
}

View File

@@ -267,7 +267,9 @@
"verifyingRecoveryKey": "Verifierar återställningsnyckel...",
"recoveryKeyVerified": "Återställningsnyckel verifierad",
"recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.",
"invalidRecoveryKey": "Återställningsnyckeln du angav är inte giltig. Kontrollera att den innehåller 24 ord och kontrollera stavningen av varje ord.\n\nOm du har angett en äldre återställningskod, se till att den är 64 tecken lång, och kontrollera var och en av bokstäverna.",
"recreatePasswordTitle": "Återskapa lösenord",
"recreatePasswordBody": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).",
"invalidKey": "Ogiltig nyckel",
"tryAgain": "Försök igen",
"viewRecoveryKey": "Visa återställningsnyckel",
@@ -279,6 +281,10 @@
"copyEmailAddress": "Kopiera e-postadress",
"exportLogs": "Exportera loggar",
"enterYourRecoveryKey": "Ange din återställningsnyckel",
"tempErrorContactSupportIfPersists": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
"networkHostLookUpErr": "Det gick inte att ansluta till Ente, kontrollera dina nätverksinställningar och kontakta supporten om felet kvarstår.",
"networkConnectionRefusedErr": "Det gick inte att ansluta till Ente, försök igen om en stund. Om felet kvarstår, vänligen kontakta support.",
"itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.",
"about": "Om",
"weAreOpenSource": "Vi är öppen källkod!",
"privacy": "Sekretess",
@@ -292,6 +298,7 @@
"checking": "Kontrollerar ...",
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
"warning": "Varning",
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
"iUnderStand": "Jag förstår",
"@iUnderStand": {
"description": "Text for the button to confirm the user understands the warning"
@@ -309,28 +316,46 @@
}
},
"sorry": "Tyvärr",
"importFailureDesc": "Det gick inte att tolka den valda filen.\nSkriv till support@ente.io om du behöver hjälp!",
"pendingSyncs": "Varning",
"pendingSyncsWarningBody": "En del av dina koder har inte säkerhetskopierats.\n\nSe till att du har en säkerhetskopia för dessa koder innan du loggar ut.",
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
"tapToEnterCode": "Tryck för att ange kod",
"resendEmail": "Skicka e-post igen",
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
"email": {
"description": "The email address of the user",
"type": "String",
"example": "example@ente.io"
}
}
},
"manualSort": "Anpassad",
"editOrder": "Redigera ordning",
"mostFrequentlyUsed": "Ofta använd",
"mostRecentlyUsed": "Senast använd",
"activeSessions": "Aktiva sessioner",
"somethingWentWrongPleaseTryAgain": "Något gick fel, vänligen försök igen",
"thisWillLogYouOutOfThisDevice": "Detta kommer att logga ut dig från den här enheten!",
"thisWillLogYouOutOfTheFollowingDevice": "Detta kommer att logga ut dig från följande enhet:",
"terminateSession": "Avsluta session?",
"terminate": "Avsluta",
"thisDevice": "Den här enheten",
"toResetVerifyEmail": "För att återställa ditt lösenord måste du först bekräfta din e-postadress.",
"thisEmailIsAlreadyInUse": "Denna e-postadress används redan",
"verificationFailedPleaseTryAgain": "Verifiering misslyckades, vänligen försök igen",
"yourVerificationCodeHasExpired": "Din verifieringskod har upphört att gälla",
"incorrectCode": "Felaktig kod",
"sorryTheCodeYouveEnteredIsIncorrect": "Tyvärr, den kod som du har angett är felaktig",
"emailChangedTo": "E-post ändrad till {newEmail}",
"authenticationFailedPleaseTryAgain": "Autentisering misslyckades, vänligen försök igen",
"authenticationSuccessful": "Autentisering lyckades!",
"twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd",
"incorrectRecoveryKey": "Felaktig återställningsnyckel",
"theRecoveryKeyYouEnteredIsIncorrect": "Återställningsnyckeln du angav är felaktig",
"enterPassword": "Ange lösenord",
"selectExportFormat": "Välj exportformat",
"encrypted": "Krypterad",
@@ -343,6 +368,7 @@
"showLargeIcons": "Visa stora ikoner",
"compactMode": "Kompakt läge",
"shouldHideCode": "Dölj koder",
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
"focusOnSearchBar": "Fokusera på sök vid appstart",
"minimizeAppOnCopy": "Minimera appen vid kopiering",
"editCodeAuthMessage": "Autentisera för att redigera kod",

View File

@@ -112,8 +112,9 @@ class Code {
String issuer,
String secret,
CodeDisplay? display,
int digits,
) {
int digits, {
Algorithm algorithm = Algorithm.sha1,
}) {
final String encodedIssuer = Uri.encodeQueryComponent(issuer);
return Code(
account,
@@ -121,10 +122,10 @@ class Code {
digits,
defaultPeriod,
secret,
Algorithm.sha1,
algorithm,
type,
0,
"otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
"otpauth://${type.name}/$issuer:$account?algorithm=${algorithm.name.toUpperCase()}&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
display: display ?? CodeDisplay(),
);
}

View File

@@ -13,6 +13,7 @@ import 'package:ente_auth/onboarding/view/common/field_label.dart';
import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/algorithm_selector_widget.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/custom_icon_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
@@ -38,10 +39,12 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final Logger _logger = Logger('_SetupEnterSecretKeyPageState');
final int _notesLimit = 500;
final int _otherTextLimit = 200;
final int defaultDigits = 6;
late TextEditingController _issuerController;
late TextEditingController _accountController;
late TextEditingController _secretController;
late TextEditingController _notesController;
late TextEditingController _digitsController;
late bool _secretKeyObscured;
late List<String> selectedTags = [...?widget.code?.display.tags];
List<String> allTags = [];
@@ -49,6 +52,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
bool isCustomIcon = false;
String _customIconID = "";
late IconType _iconSrc;
late Algorithm _algorithm;
@override
void initState() {
@@ -65,6 +69,12 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
_notesController = TextEditingController(
text: widget.code?.display.note,
);
_digitsController = TextEditingController(
text: widget.code != null
? widget.code!.digits.toString()
: defaultDigits.toString(),
);
_secretKeyObscured = widget.code != null;
_loadTags();
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
@@ -101,6 +111,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
? IconType.simpleIcon
: IconType.customIcon;
_algorithm = widget.code == null ? Algorithm.sha1 : widget.code!.algorithm;
super.initState();
}
@@ -121,6 +133,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
_issuerController.dispose();
_accountController.dispose();
_notesController.dispose();
_digitsController.dispose();
super.dispose();
}
@@ -268,6 +281,79 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
],
),
const SizedBox(height: 12),
widget.code == null
? Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
),
child: ExpansionTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
collapsedBackgroundColor: Colors.transparent,
tilePadding: EdgeInsets.zero,
title: Text(
"Advanced",
style: getEnteTextTheme(context).small,
),
children: <Widget>[
Row(
children: [
const FieldLabel("Digits"),
Expanded(
child: TextFormField(
keyboardType: TextInputType.number,
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a number";
}
final intValue = int.tryParse(value);
if (intValue == null) {
return "Only integers are allowed";
}
if (intValue < 1 || intValue > 10) {
return "OTP digits must be between 1 and 10";
}
return null;
},
maxLines: 1,
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: 12.0,
),
),
style: getEnteTextTheme(context).small,
controller: _digitsController,
),
),
],
),
const SizedBox(height: 22),
Row(
children: [
const FieldLabel("Algorithm"),
AlgorithmSelectorWidget(
currentAlgorithm: _algorithm,
onSelected: (newAlgorithm) async {
setState(() {
_algorithm = newAlgorithm;
});
},
),
],
),
const SizedBox(height: 12),
],
),
)
: const SizedBox.shrink(),
const SizedBox(height: 12),
Wrap(
spacing: 12,
alignment: WrapAlignment.start,
@@ -322,12 +408,29 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
padding: const EdgeInsets.symmetric(vertical: 8),
),
onPressed: () async {
final digits =
int.tryParse(_digitsController.text.trim());
if (digits != null && (digits < 1 || digits > 10)) {
String message = "Digits must be between 1 and 10";
_showIncorrectDetailsDialog(
context,
message: message,
);
return;
}
if ((_accountController.text.trim().isEmpty &&
_issuerController.text.trim().isEmpty) ||
_secretController.text.trim().isEmpty) {
_secretController.text.trim().isEmpty ||
_digitsController.text.trim().isEmpty ||
digits == null) {
String message;
if (_secretController.text.trim().isEmpty) {
message = context.l10n.secretCanNotBeEmpty;
} else if (_digitsController.text.isEmpty) {
message = "Digits cannot be empty";
} else if (digits == null) {
message = "Digits is not a integer";
} else {
message =
context.l10n.bothIssuerAndAccountCanNotBeEmpty;
@@ -358,6 +461,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
final issuer = _issuerController.text.trim();
final secret = _secretController.text.trim().replaceAll(' ', '');
final notes = _notesController.text.trim();
final digits = int.tryParse(_digitsController.text.trim());
final isStreamCode = issuer.toLowerCase() == "steam" ||
issuer.toLowerCase().contains('steampowered.com');
final CodeDisplay display =
@@ -398,14 +503,18 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
issuer,
secret,
display,
isStreamCode ? Code.steamDigits : Code.defaultDigits,
isStreamCode ? Code.steamDigits : digits!,
algorithm: _algorithm,
)
: widget.code!.copyWith(
account: account,
issuer: issuer,
secret: secret,
display: display,
algorithm: _algorithm,
digits: digits!,
);
// Verify the validity of the code
getOTP(newCode);
Navigator.of(context).pop(newCode);

View File

@@ -0,0 +1,72 @@
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:flutter/material.dart';
class AlgorithmSelectorWidget extends StatelessWidget {
final Algorithm currentAlgorithm;
final void Function(Algorithm) onSelected;
const AlgorithmSelectorWidget({
super.key,
required this.currentAlgorithm,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
Text algorithmOptionText(Algorithm algorithm) {
return Text(
algorithm.name.toUpperCase(),
style: getEnteTextTheme(context).small,
);
}
return GestureDetector(
onTapDown: (TapDownDetails details) async {
final int? selectedValue = await showMenu<int>(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
details.globalPosition.dy + 300,
),
items: List.generate(Algorithm.values.length, (index) {
return PopupMenuItem(
value: index,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
algorithmOptionText(Algorithm.values[index]),
if (Algorithm.values[index] == currentAlgorithm)
Icon(
Icons.check,
color: Theme.of(context).iconTheme.color,
),
],
),
);
}),
);
if (selectedValue != null) {
onSelected(Algorithm.values[selectedValue]);
}
},
child: Container(
padding: const EdgeInsets.only(bottom: 4),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Theme.of(context).dividerColor),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
algorithmOptionText(currentAlgorithm),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}
}

View File

@@ -45,7 +45,6 @@ jobs:
"${{ startsWith(github.ref, 'refs/tags/v') &&
format('photosd-{0}', github.ref_name) || ( inputs.source
|| 'main' ) }}"
submodules: recursive
- name: Setup node
uses: actions/setup-node@v4

View File

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

View File

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

View File

@@ -10,19 +10,21 @@ To know more about Ente, see [our main README](../README.md) or visit
## Building from source
Fetch submodules
Clone this repository and change to this directory
```sh
git submodule update --init --recursive
git clone https://github.com/ente-io/ente
cd ente/desktop
```
Install dependencies
Install dependencies (requires Yarn v1):
```sh
yarn install
```
Run in development mode (supports hot reload for the renderer process)
Now you can run in development mode (supports hot reload for the renderer
process)
```sh
yarn dev

View File

@@ -27,23 +27,17 @@ export default ts.config(
// Allow numbers to be used in template literals.
"@typescript-eslint/restrict-template-expressions": [
"error",
{
allowNumber: true,
},
{ allowNumber: true },
],
// Allow void expressions as the entire body of an arrow function.
"@typescript-eslint/no-confusing-void-expression": [
"error",
{
ignoreArrowShorthand: true,
},
{ ignoreArrowShorthand: true },
],
// Allow free standing ternary expressions.
"@typescript-eslint/no-unused-expressions": [
"error",
{
allowTernary: true,
},
{ allowTernary: true },
],
},
},

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.0",
"electron-log": "^5.3.2",
"electron-store": "^8.2.0",
"electron-updater": "^6.4.0",
"ffmpeg-static": "^5.2.0",
@@ -41,23 +41,22 @@
"onnxruntime-node": "^1.20.1"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@eslint/js": "^9.21.0",
"@tsconfig/node20": "^20.1.4",
"@types/auto-launch": "^5.0.5",
"@types/eslint__js": "^8.42.3",
"@types/ffmpeg-static": "^3.0.3",
"ajv": "^8.17.1",
"concurrently": "^9.1.2",
"cross-env": "^7.0.3",
"electron": "^34.1.1",
"electron": "^34.3.1",
"electron-builder": "^26.0.0",
"eslint": "^9",
"prettier": "3.4.2",
"prettier": "3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-packagejson": "^2.5.8",
"prettier-plugin-packagejson": "^2.5.10",
"shx": "^0.3.4",
"typescript": "^5.7.2",
"typescript-eslint": "^8.23.0"
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0"
},
"packageManager": "yarn@1.22.22",
"productName": "ente"

View File

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

View File

@@ -36,17 +36,9 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
{
label: "Ente Photos",
submenu: [
...macOSOnly([
{
label: "About Ente",
role: "about",
},
]),
...macOSOnly([{ label: "About Ente", role: "about" }]),
{ type: "separator" },
{
label: "Check for Updates...",
click: handleCheckForUpdates,
},
{ label: "Check for Updates...", click: handleCheckForUpdates },
{ type: "separator" },
...macOSOnly([
@@ -65,20 +57,11 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
{ type: "separator" },
...macOSOnly([
{
label: "Hide Ente",
role: "hide",
},
{
label: "Hide Others",
role: "hideOthers",
},
{ label: "Hide Ente", role: "hide" },
{ label: "Hide Others", role: "hideOthers" },
{ type: "separator" },
]),
{
label: "Quit",
role: "quit",
},
{ label: "Quit", role: "quit" },
],
},
{
@@ -96,14 +79,8 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
{
label: "Speech",
submenu: [
{
role: "startSpeaking",
label: "Start Speaking",
},
{
role: "stopSpeaking",
label: "Stop Speaking",
},
{ role: "startSpeaking", label: "Start Speaking" },
{ role: "stopSpeaking", label: "Stop Speaking" },
],
},
]),
@@ -132,15 +109,7 @@ export const createApplicationMenu = (mainWindow: BrowserWindow) => {
]),
],
},
{
label: "Help",
submenu: [
{
label: "Ente Help",
click: handleHelp,
},
],
},
{ label: "Help", submenu: [{ label: "Ente Help", click: handleHelp }] },
]);
};
@@ -159,13 +128,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
};
return Menu.buildFromTemplate([
{
label: "Open Ente",
click: handleOpen,
},
{
label: "Quit Ente",
click: handleClose,
},
{ label: "Open Ente", click: handleOpen },
{ label: "Quit Ente", click: handleClose },
]);
};

View File

@@ -10,10 +10,7 @@ class AutoLauncher {
constructor() {
if (process.platform != "darwin") {
this.autoLaunch = new AutoLaunch({
name: "ente",
isHidden: true,
});
this.autoLaunch = new AutoLaunch({ name: "ente", isHidden: true });
}
}

View File

@@ -247,9 +247,7 @@ export const computeCLIPImageEmbedding = async (
) => {
const session = await cachedCLIPImageSession();
const inputArray = new Uint8Array(input.buffer);
const feeds = {
input: new ort.Tensor("uint8", inputArray, inputShape),
};
const feeds = { input: new ort.Tensor("uint8", inputArray, inputShape) };
const t = Date.now();
const results = await session.run(feeds);
log.debugString(`ONNX/CLIP image embedding took ${Date.now() - t} ms`);
@@ -292,9 +290,7 @@ export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
const session = sessionOrSkip;
const tokenizer = getTokenizer();
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
const feeds = {
input: new ort.Tensor("int32", tokenizedText, [1, 77]),
};
const feeds = { input: new ort.Tensor("int32", tokenizedText, [1, 77]) };
const t = Date.now();
const results = await session.run(feeds);
@@ -316,9 +312,7 @@ export const detectFaces = async (
) => {
const session = await cachedFaceDetectionSession();
const inputArray = new Uint8Array(input.buffer);
const feeds = {
input: new ort.Tensor("uint8", inputArray, inputShape),
};
const feeds = { input: new ort.Tensor("uint8", inputArray, inputShape) };
const t = Date.now();
const results = await session.run(feeds);
log.debugString(`ONNX/YOLO face detection took ${Date.now() - t} ms`);

View File

@@ -84,11 +84,7 @@ export const pendingUploads = async (): Promise<PendingUploads | undefined> => {
if (filePaths.length == 0 && zipItems.length == 0) return undefined;
return {
collectionName,
filePaths,
zipItems,
};
return { collectionName, filePaths, zipItems };
};
/**

View File

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

View File

@@ -22,30 +22,13 @@ export interface UploadStatusStore {
}
const uploadStatusSchema: Schema<UploadStatusStore> = {
collectionName: {
type: "string",
},
filePaths: {
type: "array",
items: {
type: "string",
},
},
collectionName: { type: "string" },
filePaths: { type: "array", items: { type: "string" } },
zipItems: {
type: "array",
items: {
type: "array",
items: {
type: "string",
},
},
},
zipPaths: {
type: "array",
items: {
type: "string",
},
items: { type: "array", items: { type: "string" } },
},
zipPaths: { type: "array", items: { type: "string" } },
};
export const uploadStatusStore = new Store({

View File

@@ -23,12 +23,7 @@ interface UserPreferences {
* the app is not maximized (when the app was maximized when it was being
* quit then {@link isWindowMaximized} will be set instead).
*/
windowBounds?: {
x: number;
y: number;
width: number;
height: number;
};
windowBounds?: { x: number; y: number; width: number; height: number };
/**
* `true` if the app's main window is maximized the last time it was closed.
*/

View File

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

View File

@@ -125,7 +125,12 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
const { writable, readable } = new TransformStream();
const stream = await zip.stream(entry);
const nodeWritable = Writable.fromWeb(writable);
// Silence a type error about the Promise<void> returned by the close method
// of writable as not being assignable to Promise<undefined> which started
// appearing after updating to TypeScript 5.8.
//
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
const nodeWritable = Writable.fromWeb(writable as any);
stream.pipe(nodeWritable);
nodeWritable.on("error", (e: unknown) => {

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.19.0":
version "9.19.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.19.0.tgz#51dbb140ed6b49d05adc0b171c41e1a8713b7789"
integrity sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==
"@eslint/js@^9.21.0":
version "9.21.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.21.0.tgz#4303ef4e07226d87c395b8fad5278763e9c15c08"
integrity sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==
"@eslint/object-schema@^2.1.4":
version "2.1.4"
@@ -317,26 +317,6 @@
dependencies:
"@types/ms" "*"
"@types/eslint@*":
version "9.6.1"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-9.6.1.tgz#d5795ad732ce81715f27f75da913004a56751584"
integrity sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/eslint__js@^8.42.3":
version "8.42.3"
resolved "https://registry.yarnpkg.com/@types/eslint__js/-/eslint__js-8.42.3.tgz#d1fa13e5c1be63a10b4e3afe992779f81c1179a0"
integrity sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==
dependencies:
"@types/eslint" "*"
"@types/estree@*":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/ffmpeg-static@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.3.tgz#605358ac6304507a75c2fd5fd861534837b19e2f"
@@ -354,11 +334,6 @@
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==
"@types/json-schema@*":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/keyv@^3.1.4":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
@@ -417,62 +392,62 @@
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz#7745f4e3e4a7ae5f6f73fefcd856fd6a074189b7"
integrity sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==
"@typescript-eslint/eslint-plugin@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e"
integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.23.0"
"@typescript-eslint/type-utils" "8.23.0"
"@typescript-eslint/utils" "8.23.0"
"@typescript-eslint/visitor-keys" "8.23.0"
"@typescript-eslint/scope-manager" "8.26.0"
"@typescript-eslint/type-utils" "8.26.0"
"@typescript-eslint/utils" "8.26.0"
"@typescript-eslint/visitor-keys" "8.26.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
ts-api-utils "^2.0.1"
"@typescript-eslint/parser@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.23.0.tgz#57acb3b65fce48d12b70d119436e145842a30081"
integrity sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==
"@typescript-eslint/parser@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9"
integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==
dependencies:
"@typescript-eslint/scope-manager" "8.23.0"
"@typescript-eslint/types" "8.23.0"
"@typescript-eslint/typescript-estree" "8.23.0"
"@typescript-eslint/visitor-keys" "8.23.0"
"@typescript-eslint/scope-manager" "8.26.0"
"@typescript-eslint/types" "8.26.0"
"@typescript-eslint/typescript-estree" "8.26.0"
"@typescript-eslint/visitor-keys" "8.26.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.23.0.tgz#ee3bb7546421ca924b9b7a8b62a77d388193ddec"
integrity sha512-OGqo7+dXHqI7Hfm+WqkZjKjsiRtFUQHPdGMXzk5mYXhJUedO7e/Y7i8AK3MyLMgZR93TX4bIzYrfyVjLC+0VSw==
"@typescript-eslint/scope-manager@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz#b06623fad54a3a77fadab5f652ef75ed3780b545"
integrity sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==
dependencies:
"@typescript-eslint/types" "8.23.0"
"@typescript-eslint/visitor-keys" "8.23.0"
"@typescript-eslint/types" "8.26.0"
"@typescript-eslint/visitor-keys" "8.26.0"
"@typescript-eslint/type-utils@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.23.0.tgz#271e1eecece072d92679dfda5ccfceac3faa9f76"
integrity sha512-iIuLdYpQWZKbiH+RkCGc6iu+VwscP5rCtQ1lyQ7TYuKLrcZoeJVpcLiG8DliXVkUxirW/PWlmS+d6yD51L9jvA==
"@typescript-eslint/type-utils@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz#9ee8cc98184b5f66326578de9c097edc89da6f68"
integrity sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==
dependencies:
"@typescript-eslint/typescript-estree" "8.23.0"
"@typescript-eslint/utils" "8.23.0"
"@typescript-eslint/typescript-estree" "8.26.0"
"@typescript-eslint/utils" "8.26.0"
debug "^4.3.4"
ts-api-utils "^2.0.1"
"@typescript-eslint/types@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.23.0.tgz#3355f6bcc5ebab77ef6dcbbd1113ec0a683a234a"
integrity sha512-1sK4ILJbCmZOTt9k4vkoulT6/y5CHJ1qUYxqpF1K/DBAd8+ZUL4LlSCxOssuH5m4rUaaN0uS0HlVPvd45zjduQ==
"@typescript-eslint/types@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.0.tgz#c4e93a8faf3a38a8d8adb007dc7834f1c89ee7bf"
integrity sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==
"@typescript-eslint/typescript-estree@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.23.0.tgz#f633ef08efa656e386bc44b045ffcf9537cc6924"
integrity sha512-LcqzfipsB8RTvH8FX24W4UUFk1bl+0yTOf9ZA08XngFwMg4Kj8A+9hwz8Cr/ZS4KwHrmo9PJiLZkOt49vPnuvQ==
"@typescript-eslint/typescript-estree@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz#128972172005a7376e34ed2ecba4e29363b0cad1"
integrity sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==
dependencies:
"@typescript-eslint/types" "8.23.0"
"@typescript-eslint/visitor-keys" "8.23.0"
"@typescript-eslint/types" "8.26.0"
"@typescript-eslint/visitor-keys" "8.26.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -480,22 +455,22 @@
semver "^7.6.0"
ts-api-utils "^2.0.1"
"@typescript-eslint/utils@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.23.0.tgz#b269cbdc77129fd6e0e600b168b5ef740a625554"
integrity sha512-uB/+PSo6Exu02b5ZEiVtmY6RVYO7YU5xqgzTIVZwTHvvK3HsL8tZZHFaTLFtRG3CsV4A5mhOv+NZx5BlhXPyIA==
"@typescript-eslint/utils@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.0.tgz#845d20ed8378a5594e6445f54e53b972aee7b3e6"
integrity sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "8.23.0"
"@typescript-eslint/types" "8.23.0"
"@typescript-eslint/typescript-estree" "8.23.0"
"@typescript-eslint/scope-manager" "8.26.0"
"@typescript-eslint/types" "8.26.0"
"@typescript-eslint/typescript-estree" "8.26.0"
"@typescript-eslint/visitor-keys@8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.23.0.tgz#40405fd26a61d23f5f4c2ed0f016a47074781df8"
integrity sha512-oWWhcWDLwDfu++BGTZcmXWqpwtkwb5o7fxUIGksMQQDSdPW9prsSnfIOZMlsj4vBOSrcnjIUZMiIjODgGosFhQ==
"@typescript-eslint/visitor-keys@8.26.0":
version "8.26.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz#a4876216756c69130ea958df3b77222c2ad95290"
integrity sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==
dependencies:
"@typescript-eslint/types" "8.23.0"
"@typescript-eslint/types" "8.26.0"
eslint-visitor-keys "^4.2.0"
"@xmldom/xmldom@^0.8.8":
@@ -1248,10 +1223,10 @@ electron-builder@^26.0.0:
simple-update-notifier "2.0.0"
yargs "^17.6.2"
electron-log@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.0.tgz#503a911983db09156965595a7ee9a39f2d9d6384"
integrity sha512-ILgbh2k9IKbSaN8NAbQriVteEhmkdLo/e4J1dg+JIBTFzXS/kO8zNRZBh/4YPwIT/zeyxF1jP6Xz8GLsPE2IBQ==
electron-log@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.2.tgz#76aa0091f9cbf0d304546ca6f271ebb6ad953bf4"
integrity sha512-EFI5MFFEzFJU5gyhJNpKQhfGfrRP9IWzSu0sSxrWXasWKvVAOFgBySafX8W1pbPKa/w8/DDPu2bBBtVZJdDsnw==
electron-publish@26.0.0:
version "26.0.0"
@@ -1289,10 +1264,10 @@ electron-updater@^6.4.0:
semver "^7.6.3"
tiny-typed-emitter "^2.1.0"
electron@^34.1.1:
version "34.1.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-34.1.1.tgz#1fc766e406401834fedb9747c4ca58671d9a1e46"
integrity sha512-1aDYk9Gsv1/fFeClMrxWGoVMl7uCUgl1pe26BiTnLXmAoqEXCa3f3sCKFWV+cuDzUjQGAZcpkWhGYTgWUSQrLA==
electron@^34.3.1:
version "34.3.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-34.3.1.tgz#2c337a496d923463a2c7be7eaab191ad8220459b"
integrity sha512-Vsgxc4FDGg7hjduKyvTP5qfNDxZHTliZIiWD1HlR5hHXx3BFjyVv3db/uEH1GaCU0KKyeNsBXRwS4WAOMaSH5g==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@@ -2694,18 +2669,18 @@ prettier-plugin-organize-imports@^4.1.0:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f"
integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==
prettier-plugin-packagejson@^2.5.8:
version "2.5.8"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.8.tgz#1b307fce044d0230ea8f3210f8a731c5cc1b288d"
integrity sha512-BaGOF63I0IJZoudxpuQe17naV93BRtK8b3byWktkJReKEMX9CC4qdGUzThPDVO/AUhPzlqDiAXbp18U6X8wLKA==
prettier-plugin-packagejson@^2.5.10:
version "2.5.10"
resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.10.tgz#f47068d0aa12efcdddb802189d8adae874ba00e7"
integrity sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ==
dependencies:
sort-package-json "2.14.0"
sort-package-json "2.15.1"
synckit "0.9.2"
prettier@3.4.2:
version "3.4.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f"
integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==
prettier@3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5"
integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==
proc-log@^2.0.1:
version "2.0.1"
@@ -3015,10 +2990,10 @@ sort-object-keys@^1.1.3:
resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45"
integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==
sort-package-json@2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.14.0.tgz#ba0c7420dc6edea4b0eb7e9f502fda63f57586d8"
integrity sha512-xBRdmMjFB/KW3l51mP31dhlaiFmqkHLfWTfZAno8prb/wbDxwBPWFpxB16GZbiPbYr3wL41H8Kx22QIDWRe8WQ==
sort-package-json@2.15.1:
version "2.15.1"
resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.15.1.tgz#e5a035fad7da277b1947b9eecc93ea09c1c2526e"
integrity sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==
dependencies:
detect-indent "^7.0.1"
detect-newline "^4.0.0"
@@ -3234,24 +3209,24 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-eslint@^8.23.0:
version "8.23.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.23.0.tgz#796deb48f040146b68fcc8cb07db68b87219a8d2"
integrity sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ==
typescript-eslint@^8.26.0:
version "8.26.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.26.0.tgz#f44cafdaa6edc99e3612b33b791eb77a56286320"
integrity sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.23.0"
"@typescript-eslint/parser" "8.23.0"
"@typescript-eslint/utils" "8.23.0"
"@typescript-eslint/eslint-plugin" "8.26.0"
"@typescript-eslint/parser" "8.26.0"
"@typescript-eslint/utils" "8.26.0"
typescript@^5.4.3:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
typescript@^5.8.2:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
undici-types@~6.19.2:
version "6.19.8"

View File

@@ -115,4 +115,15 @@ clicking on "Your map" under "Locations" on the search screen.
## How to reset my password if I lost it?
On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password.
On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password.
# iOS Album Backup and Organization in Ente
### How does Ente handle photos that are part of multiple iOS albums?
When you select multiple albums for backup, Ente prioritizes uploading each photo to the album with the fewest photos. This means a photo will only be uploaded once, even if it exists in multiple albums on your device. If you create new albums on your device after the initial backup, those photos may not appear in the corresponding Ente album if they were already uploaded to a different album.
### Why 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

View File

@@ -22,6 +22,25 @@ In brief,
- You can invite 5 family members. So including yourself, it will be 6 people
who can share a single subscription, paying only once.
## Storage Limits
If you're an admin of a family, you will be able to set storage limits for the
members in your family plan.
In brief,
- For example, once you set a limit of 10GB for a member, their Storage
quota for uploading photos will be limited to 10GB.
- Once the invited member accepts the Family invite, you will be able to see
an edit icon in the Members List. Click on it to setup a family limit.
- If the admin has set a limit for any user, that limit value will be prefilled
in the input box.
- Incase, if you want to remove any storage limit from a members account, you
can click on the "Remove Limit" and they can upload photos without any limit.
## FAQ
- **Can you assign a storage quota for each individual member in the family

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -54,6 +54,9 @@ 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,7 +33,6 @@ After cloning the main repository with
git clone https://github.com/ente-io/ente.git
# Or git clone git@github.com:ente-io/ente.git
cd ente
git submodule update --init --recursive
```
Create a `compose.yaml` file at the root of the project with the following

View File

@@ -12,13 +12,12 @@ The getting started instructions mention using `yarn dev` (which is an alias of
>[!IMPORTANT]
> Please note that Ente's Web App supports the Yarn version 1.22.xx or 1.22.22 specifically.
> Make sure to install the right version or modify your yarn installation to meet the requirements.
> Make sure to install the right version or modify your yarn installation to meet the requirements.
> The user might end up into unknown version and dependency related errors if yarn
> is on different version.
```sh
cd ente/web
git submodule update --init --recursive
yarn install
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
```
@@ -163,7 +162,7 @@ npm install pm2@latest
Copy the below contents to a file called `ecosystem.config.js` inside the
`ente/web` directory.
```js
```js
module.exports = {
apps: [
{
@@ -212,16 +211,16 @@ pm2 start
pm2 logs all
```
## Configure App Endpoints
## Configure App Endpoints
> [!NOTE]
> [!NOTE]
> Previously, this was dependent on the env variables `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT`
> and etc. Please check the below documentation to update your setup configurations
You can configure the web endpoints for the other apps including Accounts, Albums
Family and Cast in your `museum.yaml` configuration file. Checkout
Family and Cast in your `museum.yaml` configuration file. Checkout
[`local.yaml`](https://github.com/ente-io/ente/blob/543411254b2bb55bd00a0e515dcafa12d12d3b35/server/configurations/local.yaml#L76-L89)
to configure the endpoints. Make sure to setup up your DNS Records accordingly to the
to configure the endpoints. Make sure to setup up your DNS Records accordingly to the
similar URL's you set up in `museum.yaml`.
Next part is to configure the web server.

View File

@@ -49,7 +49,6 @@ Then in a separate terminal, you can run (e.g) the web client
```sh
cd ente/web
git submodule update --init --recursive
yarn install
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev
```

View File

@@ -437,81 +437,81 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b
ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f
firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac
firebase_core: 6e223dfa350b2edceb729cea505eaaef59330682
firebase_messaging: 07fde77ae28c08616a1d4d870450efc2b38cf40d
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
maps_launcher: edf829809ba9e894d70e569bab11c16352dedb45
media_extension: 671e2567880d96c95c65c9a82ccceed8f2e309fd
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
motion_sensors: 741e702c17467b9569a92165dda8d4d88c6167f1
motionphoto: 23e2aeb5c6380112f69468d71f970fa7438e5ed1
move_to_background: 7e3467dd2a1d1013e98c9c1cb93fd53cd7ef9d84
maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203
media_extension: 6618f07abd762cdbfaadf1b0c56a287e820f0c84
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91
motionphoto: 8b65ce50c7d7ff3c767534fc3768b2eed9ac24e4
move_to_background: cd3091014529ec7829e342ad2d75c0a11f4378a5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
native_video_player: 5d36066807b680e181473e6890dde643ac85380d
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997
onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c
onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b
open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11
open_mail_app: 70273c53f768beefdafbe310c3d9086e4da3cb02
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: 0f9bc9adfc0b960e7f3bb5ec67e9a3d8193f3bdb
sentry_flutter: f4a0466dc8855998ffd59378ec33507c7dc32d7b
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sentry_flutter: 64a43fb39ab4c7f67d8a4cad52b49e22439e58b7
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
uni_links: f191d616c4db8750f74c72c988e79a83dd297fac
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
uni_links: 103d3319e3383ed8bce559b96b1e219fbf02ba96
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
video_thumbnail: 94ba6705afbaa120b77287080424930f23ea0c40
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd

View File

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

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/local/local_sync_util.dart';
import "package:photos/services/sync/import/model.dart";
import 'package:sqflite/sqlite_api.dart';
import 'package:tuple/tuple.dart';

View File

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

View File

@@ -1,6 +1,10 @@
import "dart:convert";
import "package:photos/models/file/file.dart";
import "package:photos/models/location/location.dart";
const baseRadius = 0.6;
class BaseLocation {
final List<EnteFile> files;
int? firstCreationTime;
@@ -16,6 +20,54 @@ class BaseLocation {
this.lastCreationTime,
});
static List<BaseLocation> decodeJsonToList(
String jsonString,
Map<int, EnteFile> filesMap,
) {
final jsonList = jsonDecode(jsonString) as List;
return jsonList
.map((json) => BaseLocation.fromJson(json, filesMap))
.toList();
}
static String encodeListToJson(List<BaseLocation> baseLocations) {
final jsonList =
baseLocations.map((location) => location.toJson()).toList();
return jsonEncode(jsonList);
}
static BaseLocation fromJson(
Map<String, dynamic> json,
Map<int, EnteFile> filesMap,
) {
return BaseLocation(
(json['fileIDs'] as List).map((e) => filesMap[e]!).toList(),
Location(
latitude: json['location']['latitude'],
longitude: json['location']['longitude'],
),
json['isCurrentBase'] as bool,
firstCreationTime: json['firstCreationTime'] as int?,
lastCreationTime: json['lastCreationTime'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'fileIDs': files
.where((file) => file.uploadedFileID != null)
.map((file) => file.uploadedFileID!)
.toList(),
'location': {
'latitude': location.latitude!,
'longitude': location.longitude!,
},
'isCurrentBase': isCurrentBase,
'firstCreationTime': firstCreationTime,
'lastCreationTime': lastCreationTime,
};
}
int averageCreationTime() {
if (firstCreationTime != null && lastCreationTime != null) {
return (firstCreationTime! + lastCreationTime!) ~/ 2;

View File

@@ -1,5 +1,7 @@
import "dart:convert";
import "package:photos/models/base_location.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/location/location.dart";
import "package:photos/models/memories/people_memory.dart";
import "package:photos/models/memories/smart_memory.dart";
@@ -20,18 +22,29 @@ class MemoriesCache {
final List<ToShowMemory> toShowMemories;
final List<PeopleShownLog> peopleShownLogs;
final List<TripsShownLog> tripsShownLogs;
final List<BaseLocation> baseLocations;
MemoriesCache({
required this.toShowMemories,
required this.peopleShownLogs,
required this.tripsShownLogs,
required this.baseLocations,
});
factory MemoriesCache.fromJson(Map<String, dynamic> json) {
factory MemoriesCache.fromJson(
Map<String, dynamic> json,
Map<int, EnteFile> filesMap,
) {
return MemoriesCache(
toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']),
peopleShownLogs: PeopleShownLog.decodeJsonToList(json['peopleShownLogs']),
tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']),
baseLocations: json['baseLocations'] != null
? BaseLocation.decodeJsonToList(
json['baseLocations'],
filesMap,
)
: [],
);
}
@@ -40,6 +53,7 @@ class MemoriesCache {
'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories),
'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs),
'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs),
'baseLocations': BaseLocation.encodeListToJson(baseLocations),
};
}
@@ -47,8 +61,11 @@ class MemoriesCache {
return jsonEncode(cache.toJson());
}
static MemoriesCache decodeFromJsonString(String jsonString) {
return MemoriesCache.fromJson(jsonDecode(jsonString));
static MemoriesCache decodeFromJsonString(
String jsonString,
Map<int, EnteFile> filesMap,
) {
return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap);
}
}

View File

@@ -112,7 +112,11 @@ extension SectionTypeExtensions on SectionType {
}
}
bool get sortByName => this != SectionType.face && this != SectionType.magic;
// TODO: lau: check if we should sort moment again
bool get sortByName =>
this != SectionType.face &&
this != SectionType.magic &&
this != SectionType.moment;
bool get isEmptyCTAVisible {
switch (this) {
@@ -242,6 +246,7 @@ extension SectionTypeExtensions on SectionType {
case SectionType.moment:
if (flagService.internalUser) {
// TODO: lau: remove this whole smart memories and moment altogether
return SearchService.instance.smartMemories(context, limit);
}
return SearchService.instance.getRandomMomentsSearchResults(context);

View File

@@ -11,6 +11,7 @@ import "package:photos/services/machine_learning/face_ml/face_recognition_servic
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import "package:photos/services/magic_cache_service.dart";
import "package:photos/services/memories_cache_service.dart";
import "package:photos/services/permission/service.dart";
import "package:photos/services/smart_memories_service.dart";
import "package:photos/services/storage_bonus_service.dart";
import "package:photos/services/sync/trash_sync_service.dart";
@@ -143,3 +144,9 @@ FaceRecognitionService get faceRecognitionService {
_faceRecognitionService ??= FaceRecognitionService();
return _faceRecognitionService!;
}
PermissionService? _permissionService;
PermissionService get permissionService {
_permissionService ??= PermissionService(ServiceLocator.instance.prefs);
return _permissionService!;
}

View File

@@ -10,6 +10,7 @@ import "package:photos/core/event_bus.dart";
import "package:photos/events/location_tag_updated_event.dart";
import "package:photos/extensions/stop_watch.dart";
import "package:photos/models/api/entity/type.dart";
import "package:photos/models/base_location.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/local_entity_data.dart";
import "package:photos/models/location/location.dart";
@@ -33,6 +34,8 @@ class LocationService {
List<City> _cities = [];
List<BaseLocation> baseLocations = [];
LocationService(this.prefs) {
debugPrint('LocationService constructor');
Future.delayed(const Duration(seconds: 3), () {

View File

@@ -147,11 +147,13 @@ class SemanticSearchService {
}
final textEmbedding = await _getTextEmbedding(query);
final queryResults = await _getSimilarities(
textEmbedding,
minimumSimilarity: similarityThreshold,
final similarityResults = await _getSimilarities(
{query: textEmbedding},
minimumSimilarityMap: {
query: similarityThreshold ?? kMinimumSimilarityThreshold,
},
);
final queryResults = similarityResults[query]!;
// print query for top ten scores
for (int i = 0; i < min(10, queryResults.length); i++) {
final result = queryResults[i];
@@ -196,18 +198,32 @@ class SemanticSearchService {
return results;
}
Future<List<int>> getMatchingFileIDs(
String query,
double minimumSimilarity,
Future<Map<String, List<int>>> getMatchingFileIDs(
Map<String, double> queryToScore,
) async {
final textEmbedding = await _getTextEmbedding(query);
final textEmbeddings = <String, List<double>>{};
final minimumSimilarityMap = <String, double>{};
for (final entry in queryToScore.entries) {
final query = entry.key;
final score = entry.value;
final textEmbedding = await _getTextEmbedding(query);
textEmbeddings[query] = textEmbedding;
minimumSimilarityMap[query] = score;
}
final queryResults = await _getSimilarities(
textEmbedding,
minimumSimilarity: minimumSimilarity,
textEmbeddings,
minimumSimilarityMap: minimumSimilarityMap,
);
final result = <int>[];
for (final r in queryResults) {
result.add(r.id);
final result = <String, List<int>>{};
for (final entry in queryResults.entries) {
final query = entry.key;
final queryResult = entry.value;
final fileIDs = <int>[];
for (final result in queryResult) {
fileIDs.add(result.id);
}
result[query] = fileIDs;
}
return result;
}
@@ -249,24 +265,25 @@ class SemanticSearchService {
return textEmbedding;
}
Future<List<QueryResult>> _getSimilarities(
List<double> textEmbedding, {
double? minimumSimilarity,
Future<Map<String, List<QueryResult>>> _getSimilarities(
Map<String, List<double>> textQueryToEmbeddingMap, {
required Map<String, double> minimumSimilarityMap,
}) async {
final startTime = DateTime.now();
final imageEmbeddings = await _getClipVectors();
final List<QueryResult> queryResults = await _computer.compute(
final Map<String, List<QueryResult>> queryResults = await _computer
.compute<Map<String, dynamic>, Map<String, List<QueryResult>>>(
computeBulkSimilarities,
param: {
"imageEmbeddings": imageEmbeddings,
"textEmbedding": textEmbedding,
"minimumSimilarity": minimumSimilarity,
"textQueryToEmbeddingMap": textQueryToEmbeddingMap,
"minimumSimilarityMap": minimumSimilarityMap,
},
taskName: "computeBulkSimilarities",
);
final endTime = DateTime.now();
_logger.info(
"computingSimilarities took: " +
"computingSimilarities took for ${textQueryToEmbeddingMap.length} queries " +
(endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch)
.toString() +
"ms",
@@ -293,39 +310,44 @@ class SemanticSearchService {
}
}
List<QueryResult> computeBulkSimilarities(Map args) {
final queryResults = <QueryResult>[];
Map<String, List<QueryResult>> computeBulkSimilarities(Map args) {
final imageEmbeddings = args["imageEmbeddings"] as List<EmbeddingVector>;
final textEmbedding = args["textEmbedding"] as List<double>;
final minimumSimilarity = args["minimumSimilarity"] ??
SemanticSearchService.kMinimumSimilarityThreshold;
final Vector textVector = Vector.fromList(textEmbedding);
if (!kDebugMode) {
for (final imageEmbedding in imageEmbeddings) {
final similarity = imageEmbedding.vector.dot(textVector);
if (similarity >= minimumSimilarity) {
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
final textEmbedding =
args["textQueryToEmbeddingMap"] as Map<String, List<double>>;
final minimumSimilarityMap =
args["minimumSimilarityMap"] as Map<String, double>;
final result = <String, List<QueryResult>>{};
for (final MapEntry<String, List<double>> entry in textEmbedding.entries) {
final query = entry.key;
final textVector = Vector.fromList(entry.value);
final minimumSimilarity = minimumSimilarityMap[query]!;
final queryResults = <QueryResult>[];
if (!kDebugMode) {
for (final imageEmbedding in imageEmbeddings) {
final similarity = imageEmbedding.vector.dot(textVector);
if (similarity >= minimumSimilarity) {
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
}
}
} else {
double bestScore = 0.0;
for (final imageEmbedding in imageEmbeddings) {
final similarity = imageEmbedding.vector.dot(textVector);
if (similarity >= minimumSimilarity) {
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
}
if (similarity > bestScore) {
bestScore = similarity;
}
}
if (kDebugMode && queryResults.isEmpty) {
dev.log("No results found for query with best score: $bestScore");
}
}
} else {
double bestScore = 0.0;
for (final imageEmbedding in imageEmbeddings) {
final similarity = imageEmbedding.vector.dot(textVector);
if (similarity >= minimumSimilarity) {
queryResults.add(QueryResult(imageEmbedding.fileID, similarity));
}
if (similarity > bestScore) {
bestScore = similarity;
}
}
if (kDebugMode && queryResults.isEmpty) {
dev.log("No results found for query with best score: $bestScore");
}
queryResults.sort((first, second) => second.score.compareTo(first.score));
result[query] = queryResults;
}
queryResults.sort((first, second) => second.score.compareTo(first.score));
return queryResults;
return result;
}
class QueryResult {

View File

@@ -393,14 +393,17 @@ class MagicCacheService {
Future<List<MagicCache>> _nonEmptyMagicResults(
List<Prompt> magicPromptsData,
) async {
final TimeLogger t = TimeLogger();
final results = <MagicCache>[];
final List<int> matchCount = [];
final Map<String, double> queryToScore = {};
for (Prompt prompt in magicPromptsData) {
final fileUploadedIDs =
await SemanticSearchService.instance.getMatchingFileIDs(
prompt.query,
prompt.minScore,
);
queryToScore[prompt.query] = prompt.minScore;
}
final clipResults =
await SemanticSearchService.instance.getMatchingFileIDs(queryToScore);
for (Prompt prompt in magicPromptsData) {
final List<int> fileUploadedIDs = clipResults[prompt.query] ?? [];
if (fileUploadedIDs.isNotEmpty) {
results.add(
MagicCache(prompt.title, fileUploadedIDs),
@@ -408,7 +411,7 @@ class MagicCacheService {
}
matchCount.add(fileUploadedIDs.length);
}
_logger.info('magic result count $matchCount');
_logger.info('magic result count $matchCount $t');
return results;
}
}

View File

@@ -140,26 +140,27 @@ class MemoriesCacheService {
// calculate memories for this period and for the next period
final now = DateTime.now();
final next = now.add(kMemoriesUpdateFrequency);
final nowMemories =
await smartMemoriesService.calcMemories(now, newCache);
final nextMemories =
final nowResult = await smartMemoriesService.calcMemories(now, newCache);
final nextResult =
await smartMemoriesService.calcMemories(next, newCache);
w?.log("calculated new memories");
for (final nowMemory in nowMemories) {
for (final nowMemory in nowResult.memories) {
newCache.toShowMemories
.add(ToShowMemory.fromSmartMemory(nowMemory, now));
}
for (final nextMemory in nextMemories) {
for (final nextMemory in nextResult.memories) {
newCache.toShowMemories
.add(ToShowMemory.fromSmartMemory(nextMemory, next));
}
newCache.baseLocations.addAll(nowResult.baseLocations);
w?.log("added memories to cache");
final file = File(await _getCachePath());
if (!file.existsSync()) {
file.createSync(recursive: true);
}
_cachedMemories =
nowMemories.where((memory) => memory.shouldShowNow()).toList();
nowResult.memories.where((memory) => memory.shouldShowNow()).toList();
locationService.baseLocations = nowResult.baseLocations;
await file.writeAsBytes(
MemoriesCache.encodeToJsonString(newCache).codeUnits,
);
@@ -174,8 +175,14 @@ class MemoriesCacheService {
}
}
/// WARNING: Use for testing only, TODO: lau: remove later
Future<MemoriesCache> debugCacheForTesting() async {
final oldCache = await _readCacheFromDisk();
final MemoriesCache newCache = _processOldCache(oldCache);
return newCache;
}
MemoriesCache _processOldCache(MemoriesCache? oldCache) {
final List<ToShowMemory> toShowMemories = [];
final List<PeopleShownLog> peopleShownLogs = [];
final List<TripsShownLog> tripsShownLogs = [];
if (oldCache != null) {
@@ -221,9 +228,10 @@ class MemoriesCacheService {
}
}
return MemoriesCache(
toShowMemories: toShowMemories,
toShowMemories: [],
peopleShownLogs: peopleShownLogs,
tripsShownLogs: tripsShownLogs,
baseLocations: [],
);
}
@@ -259,6 +267,7 @@ class MemoriesCacheService {
);
}
}
locationService.baseLocations = cache.baseLocations;
_logger.info('Processing of disk cache memories done');
return memories;
} catch (e, s) {
@@ -294,8 +303,17 @@ class MemoriesCacheService {
_logger.info("No memories cache found");
return null;
}
final allFiles = Set<EnteFile>.from(
await SearchService.instance.getAllFilesForSearch(),
);
final allFileIdsToFile = <int, EnteFile>{};
for (final file in allFiles) {
if (file.uploadedFileID != null) {
allFileIdsToFile[file.uploadedFileID!] = file;
}
}
final jsonString = file.readAsStringSync();
return MemoriesCache.decodeFromJsonString(jsonString);
return MemoriesCache.decodeFromJsonString(jsonString, allFileIdsToFile);
}
Future<void> clearMemoriesCache() async {

View File

@@ -0,0 +1,38 @@
import "package:photo_manager/photo_manager.dart";
import "package:shared_preferences/shared_preferences.dart";
class PermissionService {
static const kHasGrantedPermissionsKey = "has_granted_permissions";
static const kPermissionStateKey = "permission_state";
final SharedPreferences _prefs;
PermissionService(this._prefs);
Future<PermissionState> requestPhotoMangerPermissions() {
return PhotoManager.requestPermissionExtend(
requestOption: const PermissionRequestOption(
androidPermission: AndroidPermission(
type: RequestType.common,
mediaLocation: true,
),
),
);
}
bool hasGrantedPermissions() {
return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
}
bool hasGrantedLimitedPermissions() {
return _prefs.getString(kPermissionStateKey) ==
PermissionState.limited.toString();
}
bool hasGrantedFullPermission() {
return (_prefs.getString(kPermissionStateKey) ?? '') ==
PermissionState.authorized.toString();
}
Future<void> onUpdatePermission(PermissionState state) async {
await _prefs.setBool(kHasGrantedPermissionsKey, true);
await _prefs.setString(kPermissionStateKey, state.toString());
}
}

View File

@@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart";
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/extensions/user_extension.dart";
import "package:photos/models/api/collection/user.dart";
import "package:photos/models/base_location.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:photos/models/collection/collection_items.dart';
import "package:photos/models/file/extensions/file_props.dart";
@@ -1048,6 +1049,42 @@ class SearchService {
);
}
}
// Add the found base locations from the location/memories service
// TODO: lau: Add base location names
if (limit == null || tagSearchResults.length < limit) {
for (final BaseLocation base in locationService.baseLocations) {
final a = (baseRadius * scaleFactor(base.location.latitude!)) /
kilometersPerDegree;
const b = baseRadius / kilometersPerDegree;
tagSearchResults.add(
GenericSearchResult(
ResultType.location,
"Base",
base.files,
onResultTap: (ctx) {
showAddLocationSheet(
ctx,
base.location,
name: "Base",
radius: baseRadius,
);
},
hierarchicalSearchFilter: LocationFilter(
locationTag: LocationTag(
name: "Base",
radius: baseRadius,
centerPoint: base.location,
aSquare: a * a,
bSquare: b * b,
),
occurrence: kMostRelevantFilter,
matchedUploadedIDs: filesToUploadedFileIDs(base.files),
),
),
);
}
}
if (limit == null || tagSearchResults.length < limit) {
final results =
await locationService.getFilesInCity(filesWithNoLocTag, '');
@@ -1193,9 +1230,24 @@ class SearchService {
BuildContext context,
int? limit,
) async {
final memories = await memoriesCacheService.getMemories(limit);
DateTime calcTime = DateTime.now();
// await two seconds to let new page load first
await Future.delayed(const Duration(seconds: 1));
if (limit == null) {
final DateTime? pickedTime = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime(2100),
);
if (pickedTime != null) calcTime = pickedTime;
}
final cache = await memoriesCacheService.debugCacheForTesting();
final memoriesResult = await smartMemoriesService
.calcMemories(calcTime, cache, debugSurfaceAll: true);
locationService.baseLocations = memoriesResult.baseLocations;
final searchResults = <GenericSearchResult>[];
for (final memory in memories) {
for (final memory in memoriesResult.memories) {
final files = Memory.filesFromMemories(memory.memories);
searchResults.add(
GenericSearchResult(

View File

@@ -1,6 +1,7 @@
import "dart:async";
import "dart:math" show min, max;
import "package:flutter/foundation.dart" show kDebugMode;
import "package:flutter/material.dart";
import "package:intl/intl.dart";
import "package:logging/logging.dart";
@@ -9,6 +10,7 @@ import "package:photos/core/configuration.dart";
import "package:photos/core/constants.dart";
import "package:photos/db/memories_db.dart";
import "package:photos/db/ml/db.dart";
import "package:photos/extensions/stop_watch.dart";
import "package:photos/l10n/l10n.dart";
import "package:photos/models/base_location.dart";
import "package:photos/models/file/extensions/file_props.dart";
@@ -34,6 +36,13 @@ import "package:photos/services/machine_learning/ml_result.dart";
import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart";
import "package:photos/services/search_service.dart";
class MemoriesResult {
final List<SmartMemory> memories;
final List<BaseLocation> baseLocations;
MemoriesResult(this.memories, this.baseLocations);
}
class SmartMemoriesService {
final _logger = Logger("SmartMemoriesService");
final _memoriesDB = MemoriesDB.instance;
@@ -73,45 +82,58 @@ class SmartMemoriesService {
}
// One general method to get all memories, which calls on internal methods for each separate memory type
Future<List<SmartMemory>> calcMemories(
Future<MemoriesResult> calcMemories(
DateTime now,
MemoriesCache oldCache,
) async {
MemoriesCache oldCache, {
bool debugSurfaceAll = false,
}) async {
try {
_logger.finest('calcMemories called with time: $now');
final TimeLogger t = TimeLogger(context: "calcMemories");
_logger.finest('calcMemories called with time: $now $t');
await init();
final List<SmartMemory> memories = [];
final allFiles = Set<EnteFile>.from(
await SearchService.instance.getAllFilesForSearch(),
);
_seenTimes = await _memoriesDB.getSeenTimes();
_logger.finest("All files length: ${allFiles.length}");
_logger.finest("All files length: ${allFiles.length} $t");
final peopleMemories =
await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs);
final peopleMemories = await _getPeopleResults(
allFiles,
now,
oldCache.peopleShownLogs,
surfaceAll: debugSurfaceAll,
);
_deductUsedMemories(allFiles, peopleMemories);
memories.addAll(peopleMemories);
_logger.finest("All files length: ${allFiles.length}");
_logger.finest("All files length after people: ${allFiles.length} $t");
// Trip memories
final tripMemories = await _getTripsResults(allFiles, now);
final (tripMemories, bases) = await _getTripsResults(
allFiles,
now,
oldCache.tripsShownLogs,
surfaceAll: debugSurfaceAll,
);
_deductUsedMemories(allFiles, tripMemories);
memories.addAll(tripMemories);
_logger.finest("All files length: ${allFiles.length}");
_logger.finest("All files length after trips: ${allFiles.length} $t");
// Time memories
final timeMemories = await _onThisDayOrWeekResults(allFiles, now);
_deductUsedMemories(allFiles, timeMemories);
memories.addAll(timeMemories);
_logger.finest("All files length: ${allFiles.length}");
_logger.finest("All files length after time: ${allFiles.length} $t");
// Filler memories
final fillerMemories = await _getFillerResults(allFiles, now);
_deductUsedMemories(allFiles, fillerMemories);
memories.addAll(fillerMemories);
return memories;
_logger.finest("All files length after filler: ${allFiles.length} $t");
return MemoriesResult(memories, bases);
} catch (e, s) {
_logger.severe("Error calculating smart memories", e, s);
return [];
return MemoriesResult(<SmartMemory>[], <BaseLocation>[]);
}
}
@@ -129,8 +151,10 @@ class SmartMemoriesService {
Future<List<PeopleMemory>> _getPeopleResults(
Iterable<EnteFile> allFiles,
DateTime currentTime,
List<PeopleShownLog> shownPeople,
) async {
List<PeopleShownLog> shownPeople, {
bool surfaceAll = false,
}) async {
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
final List<PeopleMemory> memoryResults = [];
if (allFiles.isEmpty) return [];
final allFileIdsToFile = <int, EnteFile>{};
@@ -142,6 +166,7 @@ class SmartMemoriesService {
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
final windowEnd =
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
w?.log('allFiles setup');
// Get ordered list of important people (all named, from most to least files)
final persons = await PersonService.instance.getPersons();
@@ -169,6 +194,7 @@ class SmartMemoriesService {
final bFaces = personIdToFaceIDs[b]!.length;
return bFaces.compareTo(aFaces);
});
w?.log('orderedImportantPersonsID setup');
// Check if the user has assignmed "me"
String? meID;
@@ -179,6 +205,7 @@ class SmartMemoriesService {
break;
}
}
w?.log('meID setup part 1');
final bool isMeAssigned = meID != null;
Map<int, List<Face>>? meFilesToFaces;
if (isMeAssigned) {
@@ -187,6 +214,7 @@ class SmartMemoriesService {
meFileIDs,
);
}
w?.log('meID setup part 2');
// Loop through the people and find all memories
final Map<String, Map<PeopleMemoryType, PeopleMemory>> personToMemories =
@@ -194,10 +222,12 @@ class SmartMemoriesService {
for (final personID in orderedImportantPersonsID) {
final personFileIDs = personIdToFileIDs[personID]!;
final personName = personIdToPerson[personID]!.data.name;
w?.log('start with new person $personName');
final Map<int, List<Face>> personFilesToFaces =
await MLDataDB.instance.getFacesForFileIDs(
personFileIDs,
);
w?.log('personFilesToFaces setup');
// Inside people loop, check for spotlight (Most likely every person will have a spotlight)
final spotlightFiles = <EnteFile>[];
for (final fileID in personFileIDs) {
@@ -228,6 +258,7 @@ class SmartMemoriesService {
.putIfAbsent(personID, () => {})
.putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory);
}
w?.log('spotlight setup');
// Inside people loop, check for youAndThem
if (isMeAssigned && meID != personID) {
@@ -258,12 +289,14 @@ class SmartMemoriesService {
.putIfAbsent(personID, () => {})
.putIfAbsent(PeopleMemoryType.youAndThem, () => youAndThemMemory);
}
w?.log('youAndThem setup');
}
// Inside people loop, check for doingSomethingTogether
if (isMeAssigned && meID != personID) {
final vectors = await SemanticSearchService.instance
.getClipVectorsForFileIDs(personFileIDs);
w?.log('getting clip vectors for doingSomethingTogether');
final activityFiles = <EnteFile>[];
PeopleActivity lastActivity = PeopleActivity.values.first;
activityLoop:
@@ -277,6 +310,9 @@ class SmartMemoriesService {
}
final similarities = await MLComputer.instance
.compareEmbeddings(vectors, activityVector);
w?.log(
'comparing embeddings for doingSomethingTogether and $activity',
);
for (final fileID in personFileIDs) {
final similarity = similarities[fileID];
if (similarity == null) continue;
@@ -307,6 +343,7 @@ class SmartMemoriesService {
() => activityMemory,
);
}
w?.log('doingSomethingTogether setup');
}
// Inside people loop, check for lastTimeYouSawThem
@@ -355,16 +392,19 @@ class SmartMemoriesService {
() => lastTimeMemory,
);
}
w?.log('lastTimeYouSawThem setup');
}
// // Surface everything just for debug checking
// for (final personID in personToMemories.keys) {
// for (final memoryType in PeopleMemoryType.values) {
// if (personToMemories[personID]!.containsKey(memoryType)) {
// memoryResults.add(personToMemories[personID]![memoryType]!);
// }
// }
// }
// Surface everything just for debug checking
if (surfaceAll) {
for (final personID in personToMemories.keys) {
final personMemories = personToMemories[personID]!;
for (final memoryType in personMemories.keys) {
memoryResults.add(personMemories[memoryType]!);
}
}
return memoryResults;
}
// Loop through the people and check if we should surface anything based on relevancy (bday, last met)
personRelevancyLoop:
@@ -432,6 +472,7 @@ class SmartMemoriesService {
}
}
}
w?.log('relevancy setup');
// Loop through the people (and memory types) and add based on rotation
if (memoryResults.length >= 3) return memoryResults;
@@ -471,24 +512,30 @@ class SmartMemoriesService {
}
if (added > 0) break peopleRotationLoop;
}
w?.log('rotation setup');
return memoryResults;
}
Future<List<TripMemory>> _getTripsResults(
Future<(List<TripMemory>, List<BaseLocation>)> _getTripsResults(
Iterable<EnteFile> allFiles,
DateTime currentTime,
) async {
List<TripsShownLog> shownTrips, {
bool surfaceAll = false,
}) async {
final List<TripMemory> memoryResults = [];
final Iterable<LocalEntity<LocationTag>> locationTagEntities =
(await locationService.getLocationTags());
if (allFiles.isEmpty) return [];
if (allFiles.isEmpty) return (<TripMemory>[], <BaseLocation>[]);
final nowInMicroseconds = currentTime.microsecondsSinceEpoch;
final windowEnd =
currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch;
final currentMonth = currentTime.month;
final cutOffTime = currentTime.subtract(const Duration(days: 365));
const tripRadius = 100.0;
const overlapRadius = 10.0;
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
for (int i = 0; i < locationTagEntities.length; i++) {
tagToItemsMap[locationTagEntities.elementAt(i)] = [];
@@ -496,12 +543,13 @@ class SmartMemoriesService {
final List<(List<EnteFile>, Location)> smallRadiusClusters = [];
final List<(List<EnteFile>, Location)> wideRadiusClusters = [];
// Go through all files and cluster the ones not inside any location tag
allFilesLoop:
for (EnteFile file in allFiles) {
if (!file.hasLocation ||
file.uploadedFileID == null ||
!file.isOwner ||
file.creationTime == null) {
continue;
continue allFilesLoop;
}
// Check if the file is inside any location tag
bool hasLocationTag = false;
@@ -516,42 +564,41 @@ class SmartMemoriesService {
}
}
// Cluster the files not inside any location tag (incremental clustering)
if (!hasLocationTag) {
// Small radius clustering for base locations
bool foundSmallCluster = false;
for (final cluster in smallRadiusClusters) {
final clusterLocation = cluster.$2;
if (isFileInsideLocationTag(
clusterLocation,
file.location!,
0.6,
)) {
cluster.$1.add(file);
foundSmallCluster = true;
break;
}
if (hasLocationTag) continue allFilesLoop;
// Small radius clustering for base locations
bool addedToExistingSmallCluster = false;
for (final cluster in smallRadiusClusters) {
final clusterLocation = cluster.$2;
if (isFileInsideLocationTag(
clusterLocation,
file.location!,
baseRadius,
)) {
cluster.$1.add(file);
addedToExistingSmallCluster = true;
break;
}
if (!foundSmallCluster) {
smallRadiusClusters.add(([file], file.location!));
}
// Wide radius clustering for trip locations
bool foundWideCluster = false;
for (final cluster in wideRadiusClusters) {
final clusterLocation = cluster.$2;
if (isFileInsideLocationTag(
clusterLocation,
file.location!,
100.0,
)) {
cluster.$1.add(file);
foundWideCluster = true;
break;
}
}
if (!foundWideCluster) {
wideRadiusClusters.add(([file], file.location!));
}
if (!addedToExistingSmallCluster) {
smallRadiusClusters.add(([file], file.location!));
}
// Wide radius clustering for trip locations
bool addedToExistingWideCluster = false;
for (final cluster in wideRadiusClusters) {
final clusterLocation = cluster.$2;
if (isFileInsideLocationTag(
clusterLocation,
file.location!,
tripRadius,
)) {
cluster.$1.add(file);
addedToExistingWideCluster = true;
break;
}
}
if (!addedToExistingWideCluster) {
wideRadiusClusters.add(([file], file.location!));
}
}
// Identify base locations
@@ -577,12 +624,20 @@ class SmartMemoriesService {
final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch(
creationTimes.last,
);
if (lastCreationTime.difference(firstCreationTime).inDays < 90) {
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
if (daysRange < 90) {
continue;
}
// Check for a minimum average number of days photos are clicked in range
final daysRange = lastCreationTime.difference(firstCreationTime).inDays;
if (uniqueDays.length < daysRange * 0.1) continue;
// Check that there isn't a huge time gap somewhere in the range
final int gapThreshold = (daysRange * 0.6).round() * microSecondsInDay;
int maxGap = 0;
for (int i = 1; i < creationTimes.length; i++) {
final gap = creationTimes[i] - creationTimes[i - 1];
if (gap > maxGap) maxGap = gap;
}
if (maxGap > gapThreshold) continue;
// Check if it's a current or old base location
final bool isCurrent = lastCreationTime.isAfter(
DateTime.now().subtract(
@@ -604,7 +659,7 @@ class SmartMemoriesService {
if (isFileInsideLocationTag(
baseLocation.location,
location,
10.0,
overlapRadius,
)) {
tooClose = true;
break;
@@ -614,7 +669,7 @@ class SmartMemoriesService {
if (isFileInsideLocationTag(
tag.item.centerPoint,
location,
10.0,
overlapRadius,
)) {
tooClose = true;
break;
@@ -753,25 +808,51 @@ class SmartMemoriesService {
}
// For now for testing let's just surface all base locations
for (final baseLocation in baseLocations) {
String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
final String? locationName = _tryFindLocationName(
Memory.fromFiles(baseLocation.files, _seenTimes),
base: true,
);
if (locationName != null) {
name =
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
}
memoryResults.add(
TripMemory(
// For now surface these on the location section TODO: lau: remove internal flag title
if (surfaceAll) {
for (final baseLocation in baseLocations) {
String name =
"Base (${baseLocation.isCurrentBase ? 'current' : 'old'})";
final String? locationName = _tryFindLocationName(
Memory.fromFiles(baseLocation.files, _seenTimes),
name,
0,
0,
baseLocation.location,
),
);
base: true,
);
if (locationName != null) {
name =
"$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})";
}
memoryResults.add(
TripMemory(
Memory.fromFiles(baseLocation.files, _seenTimes),
name,
nowInMicroseconds,
windowEnd,
baseLocation.location,
),
);
}
for (final trip in validTrips) {
final year = DateTime.fromMicrosecondsSinceEpoch(
trip.averageCreationTime(),
).year;
final String? locationName = _tryFindLocationName(trip.memories);
String name = "Trip in $year";
if (locationName != null) {
name = "Trip to $locationName";
} else if (year == currentTime.year - 1) {
name = "Last year's trip";
}
final photoSelection = await _bestSelection(trip.memories);
memoryResults.add(
trip.copyWith(
memories: photoSelection,
title: name,
firstDateToShow: nowInMicroseconds,
lastDateToShow: windowEnd,
),
);
}
return (memoryResults, baseLocations);
}
// For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip
@@ -844,17 +925,14 @@ class SmartMemoriesService {
// Otherwise, if no trips happened in the current month,
// look for the earliest upcoming trip in another month that has 3+ trips.
else {
// TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months
final sortedUpcomingMonths =
List<int>.generate(12, (i) => ((currentMonth + i) % 12) + 1);
List<int>.generate(6, (i) => ((currentMonth + i) % 12) + 1);
checkUpcomingMonths:
for (final month in sortedUpcomingMonths) {
if (tripsByMonthYear.containsKey(month)) {
final List<TripMemory> thatMonthTrips = [];
for (final trips in tripsByMonthYear[month]!.values) {
for (final trip in trips) {
thatMonthTrips.add(trip);
}
thatMonthTrips.addAll(trips);
}
if (thatMonthTrips.length >= 3) {
// take and use the third earliest trip
@@ -862,32 +940,46 @@ class SmartMemoriesService {
(a, b) =>
a.averageCreationTime().compareTo(b.averageCreationTime()),
);
final trip = thatMonthTrips[2];
final year =
DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime())
.year;
final String? locationName = _tryFindLocationName(trip.memories);
String name = "Trip in $year";
if (locationName != null) {
name = "Trip to $locationName";
} else if (year == currentTime.year - 1) {
name = "Last year's trip";
checkPotentialTrips:
for (final trip in thatMonthTrips.sublist(2)) {
for (final shownTrip in shownTrips) {
final distance =
calculateDistance(trip.location, shownTrip.location);
final shownTripDate = DateTime.fromMicrosecondsSinceEpoch(
shownTrip.lastTimeShown,
);
final shownRecently =
currentTime.difference(shownTripDate) < kTripShowTimeout;
if (distance < overlapRadius && shownRecently) {
continue checkPotentialTrips;
}
}
final year = DateTime.fromMicrosecondsSinceEpoch(
trip.averageCreationTime(),
).year;
final String? locationName = _tryFindLocationName(trip.memories);
String name = "Trip in $year";
if (locationName != null) {
name = "Trip to $locationName";
} else if (year == currentTime.year - 1) {
name = "Last year's trip";
}
final photoSelection = await _bestSelection(trip.memories);
memoryResults.add(
trip.copyWith(
memories: photoSelection,
title: name,
firstDateToShow: nowInMicroseconds,
lastDateToShow: windowEnd,
),
);
break checkUpcomingMonths;
}
final photoSelection = await _bestSelection(trip.memories);
memoryResults.add(
trip.copyWith(
memories: photoSelection,
title: name,
firstDateToShow: nowInMicroseconds,
lastDateToShow: windowEnd,
),
);
break checkUpcomingMonths;
}
}
}
}
return memoryResults;
return (memoryResults, baseLocations);
}
Future<List<TimeMemory>> _onThisDayOrWeekResults(
@@ -1221,6 +1313,7 @@ class SmartMemoriesService {
int? prefferedSize,
}) async {
try {
final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start();
final fileCount = memories.length;
final int targetSize = prefferedSize ?? 10;
if (fileCount <= targetSize) return memories;
@@ -1315,6 +1408,7 @@ class SmartMemoriesService {
_logger.finest(
'People memories selection done, returning ${finalSelection.length} memories',
);
w?.log('People memories selection done');
return finalSelection;
} catch (e, s) {
_logger.severe('Error in _bestSelectionPeople', e, s);
@@ -1354,9 +1448,9 @@ class SmartMemoriesService {
final fileToScore = await MLComputer.instance
.compareEmbeddings(vectors, _clipPositiveTextVector!);
final fileIdToClip = <int, EmbeddingVector>{};
for (final vector in vectors) {
fileIdToClip[vector.fileID] = vector;
}
for (final vector in vectors) {
fileIdToClip[vector.fileID] = vector;
}
// Get face scores for each file
final fileToFaceCount = <int, int>{};

View File

@@ -0,0 +1,120 @@
import "package:computer/computer.dart";
import "package:logging/logging.dart";
import "package:photo_manager/photo_manager.dart";
import "package:photos/models/file/file.dart";
import "package:photos/services/sync/import/model.dart";
class LocalDiffResult {
// unique localPath Assets.
final List<LocalPathAsset>? localPathAssets;
// set of File object created from localPathAssets
List<EnteFile>? uniqueLocalFiles;
// newPathToLocalIDs represents new entries which needs to be synced to
// the local db
final Map<String, Set<String>>? newPathToLocalIDs;
final Map<String, Set<String>>? deletePathToLocalIDs;
LocalDiffResult({
this.uniqueLocalFiles,
this.localPathAssets,
this.newPathToLocalIDs,
this.deletePathToLocalIDs,
});
}
Future<LocalDiffResult> getDiffFromExistingImport(
List<LocalPathAsset> assets,
// current set of assets available on device
Set<String> existingIDs, // localIDs of files already imported in app
Map<String, Set<String>> pathToLocalIDs,
) async {
final Map<String, dynamic> args = <String, dynamic>{};
args['assets'] = assets;
args['existingIDs'] = existingIDs;
args['pathToLocalIDs'] = pathToLocalIDs;
final LocalDiffResult diffResult = await Computer.shared().compute(
_getLocalAssetsDiff,
param: args,
taskName: "getLocalAssetsDiff",
);
if (diffResult.localPathAssets != null) {
diffResult.uniqueLocalFiles =
await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets!);
}
return diffResult;
}
Future<List<EnteFile>> _convertLocalAssetsToUniqueFiles(
List<LocalPathAsset> assets,
) async {
final Set<String> alreadySeenLocalIDs = <String>{};
final List<EnteFile> files = [];
for (LocalPathAsset localPathAsset in assets) {
final String localPathName = localPathAsset.pathName;
for (final String localID in localPathAsset.localIDs) {
if (!alreadySeenLocalIDs.contains(localID)) {
final assetEntity = await AssetEntity.fromId(localID);
if (assetEntity == null) {
Logger("_convertLocalAssetsToUniqueFiles")
.warning('Failed to fetch asset with id $localID');
continue;
}
files.add(
await EnteFile.fromAsset(localPathName, assetEntity),
);
alreadySeenLocalIDs.add(localID);
}
}
}
return files;
}
// _getLocalAssetsDiff compares local db with the file system and compute
// the files which needs to be added or removed from device collection.
LocalDiffResult _getLocalAssetsDiff(Map<String, dynamic> args) {
final List<LocalPathAsset> onDeviceLocalPathAsset = args['assets'];
final Set<String> existingIDs = args['existingIDs'];
final Map<String, Set<String>> pathToLocalIDs = args['pathToLocalIDs'];
final Map<String, Set<String>> newPathToLocalIDs = <String, Set<String>>{};
final Map<String, Set<String>> removedPathToLocalIDs =
<String, Set<String>>{};
final List<LocalPathAsset> unsyncedAssets = [];
for (final localPathAsset in onDeviceLocalPathAsset) {
final String pathID = localPathAsset.pathID;
// Start identifying pathID to localID mapping changes which needs to be
// synced
final Set<String> candidateLocalIDsForRemoval =
pathToLocalIDs[pathID] ?? <String>{};
final Set<String> missingLocalIDsInPath = <String>{};
for (final String localID in localPathAsset.localIDs) {
if (candidateLocalIDsForRemoval.contains(localID)) {
// remove the localID after checking. Any pending existing ID indicates
// the the local file was removed from the path.
candidateLocalIDsForRemoval.remove(localID);
} else {
missingLocalIDsInPath.add(localID);
}
}
if (candidateLocalIDsForRemoval.isNotEmpty) {
removedPathToLocalIDs[pathID] = candidateLocalIDsForRemoval;
}
if (missingLocalIDsInPath.isNotEmpty) {
newPathToLocalIDs[pathID] = missingLocalIDsInPath;
}
// End
localPathAsset.localIDs.removeAll(existingIDs);
if (localPathAsset.localIDs.isNotEmpty) {
unsyncedAssets.add(localPathAsset);
}
}
return LocalDiffResult(
localPathAssets: unsyncedAssets,
newPathToLocalIDs: newPathToLocalIDs,
deletePathToLocalIDs: removedPathToLocalIDs,
);
}

View File

@@ -7,6 +7,7 @@ import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/local_import_progress.dart';
import 'package:photos/models/file/file.dart';
import "package:photos/services/sync/import/model.dart";
import 'package:tuple/tuple.dart';
final _logger = Logger("FileSyncUtil");
@@ -127,99 +128,6 @@ Future<List<LocalPathAsset>> getAllLocalAssets() async {
return localPathAssets;
}
Future<LocalDiffResult> getDiffWithLocal(
List<LocalPathAsset> assets,
// current set of assets available on device
Set<String> existingIDs, // localIDs of files already imported in app
Map<String, Set<String>> pathToLocalIDs,
) async {
final Map<String, dynamic> args = <String, dynamic>{};
args['assets'] = assets;
args['existingIDs'] = existingIDs;
args['pathToLocalIDs'] = pathToLocalIDs;
final LocalDiffResult diffResult = await Computer.shared().compute(
_getLocalAssetsDiff,
param: args,
taskName: "getLocalAssetsDiff",
);
if (diffResult.localPathAssets != null) {
diffResult.uniqueLocalFiles =
await _convertLocalAssetsToUniqueFiles(diffResult.localPathAssets!);
}
return diffResult;
}
// _getLocalAssetsDiff compares local db with the file system and compute
// the files which needs to be added or removed from device collection.
LocalDiffResult _getLocalAssetsDiff(Map<String, dynamic> args) {
final List<LocalPathAsset> onDeviceLocalPathAsset = args['assets'];
final Set<String> existingIDs = args['existingIDs'];
final Map<String, Set<String>> pathToLocalIDs = args['pathToLocalIDs'];
final Map<String, Set<String>> newPathToLocalIDs = <String, Set<String>>{};
final Map<String, Set<String>> removedPathToLocalIDs =
<String, Set<String>>{};
final List<LocalPathAsset> unsyncedAssets = [];
for (final localPathAsset in onDeviceLocalPathAsset) {
final String pathID = localPathAsset.pathID;
// Start identifying pathID to localID mapping changes which needs to be
// synced
final Set<String> candidateLocalIDsForRemoval =
pathToLocalIDs[pathID] ?? <String>{};
final Set<String> missingLocalIDsInPath = <String>{};
for (final String localID in localPathAsset.localIDs) {
if (candidateLocalIDsForRemoval.contains(localID)) {
// remove the localID after checking. Any pending existing ID indicates
// the the local file was removed from the path.
candidateLocalIDsForRemoval.remove(localID);
} else {
missingLocalIDsInPath.add(localID);
}
}
if (candidateLocalIDsForRemoval.isNotEmpty) {
removedPathToLocalIDs[pathID] = candidateLocalIDsForRemoval;
}
if (missingLocalIDsInPath.isNotEmpty) {
newPathToLocalIDs[pathID] = missingLocalIDsInPath;
}
// End
localPathAsset.localIDs.removeAll(existingIDs);
if (localPathAsset.localIDs.isNotEmpty) {
unsyncedAssets.add(localPathAsset);
}
}
return LocalDiffResult(
localPathAssets: unsyncedAssets,
newPathToLocalIDs: newPathToLocalIDs,
deletePathToLocalIDs: removedPathToLocalIDs,
);
}
Future<List<EnteFile>> _convertLocalAssetsToUniqueFiles(
List<LocalPathAsset> assets,
) async {
final Set<String> alreadySeenLocalIDs = <String>{};
final List<EnteFile> files = [];
for (LocalPathAsset localPathAsset in assets) {
final String localPathName = localPathAsset.pathName;
for (final String localID in localPathAsset.localIDs) {
if (!alreadySeenLocalIDs.contains(localID)) {
final assetEntity = await AssetEntity.fromId(localID);
if (assetEntity == null) {
_logger.warning('Failed to fetch asset with id $localID');
continue;
}
files.add(
await EnteFile.fromAsset(localPathName, assetEntity),
);
alreadySeenLocalIDs.add(localID);
}
}
}
return files;
}
/// returns a list of AssetPathEntity with relevant filter operations.
/// [needTitle] impacts the performance for fetching the actual [AssetEntity]
/// in iOS. Same is true for [containsModifiedPath]
@@ -314,36 +222,3 @@ Future<Tuple2<Set<String>, List<EnteFile>>> _getLocalIDsAndFilesFromAssets(
}
return Tuple2(localIDs, files);
}
class LocalPathAsset {
final Set<String> localIDs;
final String pathID;
final String pathName;
LocalPathAsset({
required this.localIDs,
required this.pathName,
required this.pathID,
});
}
class LocalDiffResult {
// unique localPath Assets.
final List<LocalPathAsset>? localPathAssets;
// set of File object created from localPathAssets
List<EnteFile>? uniqueLocalFiles;
// newPathToLocalIDs represents new entries which needs to be synced to
// the local db
final Map<String, Set<String>>? newPathToLocalIDs;
final Map<String, Set<String>>? deletePathToLocalIDs;
LocalDiffResult({
this.uniqueLocalFiles,
this.localPathAssets,
this.newPathToLocalIDs,
this.deletePathToLocalIDs,
});
}

View File

@@ -0,0 +1,11 @@
class LocalPathAsset {
final Set<String> localIDs;
final String pathID;
final String pathName;
LocalPathAsset({
required this.localIDs,
required this.pathName,
required this.pathID,
});
}

View File

@@ -13,14 +13,17 @@ import 'package:photos/db/file_updation_db.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/backup_folders_updated_event.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/events/permission_granted_event.dart";
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/extensions/stop_watch.dart';
import 'package:photos/models/file/file.dart';
import "package:photos/models/ignored_file.dart";
import "package:photos/service_locator.dart";
import 'package:photos/services/app_lifecycle_service.dart';
import "package:photos/services/ignored_files_service.dart";
import 'package:photos/services/local/local_sync_util.dart';
import "package:photos/utils/photo_manager_util.dart";
import "package:photos/services/sync/import/diff.dart";
import "package:photos/services/sync/import/local_assets.dart";
import "package:photos/services/sync/import/model.dart";
import "package:photos/utils/standalone/debouncer.dart";
import 'package:shared_preferences/shared_preferences.dart';
import 'package:synchronized/synchronized.dart';
@@ -36,8 +39,6 @@ class LocalSyncService {
static const kDbUpdationTimeKey = "db_updation_time";
static const kHasCompletedFirstImportKey = "has_completed_firstImport";
static const kHasGrantedPermissionsKey = "has_granted_permissions";
static const kPermissionStateKey = "permission_state";
LocalSyncService._privateConstructor();
@@ -49,18 +50,23 @@ class LocalSyncService {
if (!AppLifecycleService.instance.isForeground) {
await PhotoManager.setIgnorePermissionCheck(true);
}
if (hasGrantedPermissions()) {
if (permissionService.hasGrantedPermissions()) {
_registerChangeCallback();
} else {
Bus.instance.on<PermissionGrantedEvent>().listen((event) async {
_registerChangeCallback();
});
}
}
Future<void> sync() async {
if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
if (!permissionService.hasGrantedPermissions()) {
_logger.info("Skipping local sync since permission has not been granted");
return;
}
if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
final permissionState = await requestPhotoMangerPermissions();
final permissionState =
await permissionService.requestPhotoMangerPermissions();
if (permissionState != PermissionState.authorized) {
_logger.severe(
"sync requested with invalid permission",
@@ -168,7 +174,7 @@ class LocalSyncService {
final Map<String, Set<String>> pathToLocalIDs =
await _db.getDevicePathIDToLocalIDMap();
final localDiffResult = await getDiffWithLocal(
final localDiffResult = await getDiffFromExistingImport(
localAssets,
existingLocalFileIDs,
pathToLocalIDs,
@@ -237,36 +243,6 @@ class LocalSyncService {
return _lock;
}
bool hasGrantedPermissions() {
return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
}
bool hasGrantedLimitedPermissions() {
return _prefs.getString(kPermissionStateKey) ==
PermissionState.limited.toString();
}
bool hasGrantedFullPermission() {
return (_prefs.getString(kPermissionStateKey) ?? '') ==
PermissionState.authorized.toString();
}
Future<void> onUpdatePermission(PermissionState state) async {
await _prefs.setBool(kHasGrantedPermissionsKey, true);
await _prefs.setString(kPermissionStateKey, state.toString());
}
Future<void> onPermissionGranted(PermissionState state) async {
await _prefs.setBool(kHasGrantedPermissionsKey, true);
await _prefs.setString(kPermissionStateKey, state.toString());
if (state == PermissionState.limited) {
// when limited permission is granted, by default mark all folders for
// backup
await Configuration.instance.setSelectAllFoldersForBackup(true);
}
_registerChangeCallback();
}
bool hasCompletedFirstImport() {
return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
}
@@ -365,7 +341,7 @@ class LocalSyncService {
if (_existingSync != null) {
await _existingSync!.future;
}
if (hasGrantedLimitedPermissions()) {
if (permissionService.hasGrantedLimitedPermissions()) {
unawaited(syncAll());
} else {
unawaited(sync().then((value) => _refreshDeviceFolderCountAndCover()));

View File

@@ -9,7 +9,6 @@ import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/permission_granted_event.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import 'package:photos/events/sync_status_update_event.dart';
import 'package:photos/events/trigger_logout_event.dart';
@@ -169,10 +168,7 @@ class SyncService {
return _lastSyncStatusEvent;
}
Future<void> onPermissionGranted(PermissionState state) async {
_logger.info("Permission granted " + state.toString());
await _localSyncService.onPermissionGranted(state);
Bus.instance.fire(PermissionGrantedEvent());
Future<void> onPermissionGranted() async {
_doSync().ignore();
}

View File

@@ -0,0 +1,37 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class InheritedDetailPageState extends InheritedWidget {
final enableFullScreenNotifier = ValueNotifier(false);
InheritedDetailPageState({
super.key,
required super.child,
});
static InheritedDetailPageState of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<InheritedDetailPageState>()!;
void toggleFullScreen({bool? shouldEnable}) {
if (shouldEnable != null) {
if (enableFullScreenNotifier.value == shouldEnable) return;
}
enableFullScreenNotifier.value = !enableFullScreenNotifier.value;
if (enableFullScreenNotifier.value) {
Future.delayed(const Duration(milliseconds: 200), () {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [],
);
});
} else {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
overlays: SystemUiOverlay.values,
);
}
}
@override
bool updateShouldNotify(InheritedDetailPageState oldWidget) =>
oldWidget.enableFullScreenNotifier != enableFullScreenNotifier;
}

View File

@@ -5,13 +5,11 @@ import 'package:flutter/material.dart';
import "package:logging/logging.dart";
import "package:photo_manager/photo_manager.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/services/sync/local_sync_service.dart";
import "package:photos/service_locator.dart";
import 'package:photos/ui/components/buttons/icon_button_widget.dart';
import "package:photos/ui/settings/backup/backup_folder_selection_page.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/navigation_util.dart";
import "package:photos/utils/photo_manager_util.dart";
class HomeHeaderWidget extends StatefulWidget {
final Widget centerWidget;
const HomeHeaderWidget({required this.centerWidget, super.key});
@@ -48,20 +46,20 @@ class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
onTap: () async {
try {
final PermissionState state =
await requestPhotoMangerPermissions();
await LocalSyncService.instance.onUpdatePermission(state);
await permissionService.requestPhotoMangerPermissions();
await permissionService.onUpdatePermission(state);
} on Exception catch (e) {
Logger("HomeHeaderWidget").severe(
"Failed to request permission: ${e.toString()}",
e,
);
}
if (!LocalSyncService.instance.hasGrantedFullPermission()) {
if (!permissionService.hasGrantedFullPermission()) {
if (Platform.isAndroid) {
await PhotoManager.openSetting();
} else {
final bool hasGrantedLimit =
LocalSyncService.instance.hasGrantedLimitedPermissions();
permissionService.hasGrantedLimitedPermissions();
// ignore: unawaited_futures
showChoiceActionSheet(
context,

View File

@@ -4,12 +4,15 @@ import 'dart:io';
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
import 'package:photo_manager/photo_manager.dart';
import "package:photos/core/configuration.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/permission_granted_event.dart";
import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import "package:photos/service_locator.dart";
import 'package:photos/services/sync/sync_service.dart';
import "package:photos/theme/ente_theme.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/photo_manager_util.dart";
import "package:styled_text/styled_text.dart";
class GrantPermissionsWidget extends StatefulWidget {
@@ -106,11 +109,12 @@ class _GrantPermissionsWidgetState extends State<GrantPermissionsWidget> {
child: Text(S.of(context).grantPermission),
onPressed: () async {
try {
final state = await requestPhotoMangerPermissions();
final state =
await permissionService.requestPhotoMangerPermissions();
_logger.info("Permission state: $state");
if (state == PermissionState.authorized ||
state == PermissionState.limited) {
await SyncService.instance.onPermissionGranted(state);
await onPermissionGranted(state);
} else if (state == PermissionState.denied) {
await showChoiceDialog(
context,
@@ -139,4 +143,16 @@ class _GrantPermissionsWidgetState extends State<GrantPermissionsWidget> {
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Future<void> onPermissionGranted(PermissionState state) async {
_logger.info("Permission granted " + state.toString());
await permissionService.onUpdatePermission(state);
if (state == PermissionState.limited) {
// when limited permission is granted, by default mark all folders for
// backup
await Configuration.instance.setSelectAllFoldersForBackup(true);
}
SyncService.instance.onPermissionGranted().ignore();
Bus.instance.fire(PermissionGrantedEvent());
}
}

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/services/sync/local_sync_service.dart';
import "package:photos/service_locator.dart";
import 'package:photos/ui/common/bottom_shadow.dart';
import "package:photos/ui/components/buttons/button_widget.dart";
import "package:photos/ui/components/dialog_widget.dart";
@@ -44,7 +44,7 @@ class _LoadingPhotosWidgetState extends State<LoadingPhotosWidget> {
_firstImportEvent =
Bus.instance.on<SyncStatusUpdate>().listen((event) async {
if (mounted && event.status == SyncStatus.completedFirstGalleryImport) {
if (LocalSyncService.instance.hasGrantedLimitedPermissions()) {
if (permissionService.hasGrantedLimitedPermissions()) {
// Do nothing, let HomeWidget refresh
} else {
// ignore: unawaited_futures

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/services/sync/local_sync_service.dart';
import "package:photos/service_locator.dart";
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/settings/backup/backup_folder_selection_page.dart';
import 'package:photos/utils/navigation_util.dart';
@@ -42,8 +42,7 @@ class StartBackupHookWidget extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: GradientButton(
onTap: () async {
if (LocalSyncService.instance
.hasGrantedLimitedPermissions()) {
if (permissionService.hasGrantedLimitedPermissions()) {
unawaited(PhotoManager.presentLimited());
} else {
// ignore: unawaited_futures

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 (!LocalSyncService.instance.hasGrantedLimitedPermissions()) {
if (!permissionService.hasGrantedLimitedPermissions()) {
delayInRefresh = const Duration(milliseconds: 250);
}
Future.delayed(
@@ -643,7 +643,7 @@ class _HomeWidgetState extends State<HomeWidget> {
_closeDrawerIfOpen(context);
return const LandingPageWidget();
}
if (!LocalSyncService.instance.hasGrantedPermissions()) {
if (!permissionService.hasGrantedPermissions()) {
entityService.syncEntities().then((_) {
PersonService.instance.resetEmailToPartialPersonDataCache();
});
@@ -671,7 +671,7 @@ class _HomeWidgetState extends State<HomeWidget> {
_showShowBackupHook =
!Configuration.instance.hasSelectedAnyBackupFolder() &&
!LocalSyncService.instance.hasGrantedLimitedPermissions() &&
!permissionService.hasGrantedLimitedPermissions() &&
CollectionsService.instance.getActiveCollections().isEmpty;
return Stack(

View File

@@ -16,6 +16,7 @@ import 'package:photos/models/file/file.dart';
import "package:photos/models/file/file_type.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/local_authentication_service.dart";
import "package:photos/states/detail_page_state.dart";
import "package:photos/ui/common/fast_scroll_physics.dart";
import 'package:photos/ui/notification/toast.dart';
import 'package:photos/ui/tools/editor/image_editor_page.dart';
@@ -77,7 +78,6 @@ class _DetailPageState extends State<DetailPage> {
List<EnteFile>? _files;
late PageController _pageController;
final _selectedIndexNotifier = ValueNotifier(0);
final _enableFullScreenNotifier = ValueNotifier(false);
bool _isFirstOpened = true;
bool isGuestView = false;
bool swipeLocked = false;
@@ -103,7 +103,6 @@ class _DetailPageState extends State<DetailPage> {
void dispose() {
_guestViewEventSubscription.cancel();
_pageController.dispose();
_enableFullScreenNotifier.dispose();
_selectedIndexNotifier.dispose();
super.dispose();
@@ -137,96 +136,102 @@ class _DetailPageState extends State<DetailPage> {
_files!.length.toString() +
" files .",
);
return PopScope(
canPop: !isGuestView,
onPopInvoked: (didPop) async {
if (isGuestView) {
final authenticated = await _requestAuthentication();
if (authenticated) {
Bus.instance.fire(GuestViewEvent(false, false));
await localSettings.setOnGuestView(false);
return InheritedDetailPageState(
child: PopScope(
canPop: !isGuestView,
onPopInvoked: (didPop) async {
if (isGuestView) {
final authenticated = await _requestAuthentication();
if (authenticated) {
Bus.instance.fire(GuestViewEvent(false, false));
await localSettings.setOnGuestView(false);
}
}
}
},
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileAppBar(
_files![selectedIndex],
_onFileRemoved,
widget.config.mode == DetailPageMode.full,
enableFullScreenNotifier: _enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
},
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(80),
child: ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileAppBar(
_files![selectedIndex],
_onFileRemoved,
widget.config.mode == DetailPageMode.full,
enableFullScreenNotifier: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
),
),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
_buildPageView(context),
ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
_onEditFileRequested,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
userID: Configuration.instance.getUserID(),
enableFullScreenNotifier: _enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
ValueListenableBuilder(
valueListenable: _selectedIndexNotifier,
builder: (BuildContext context, int selectedIndex, _) {
if (_files![selectedIndex].isPanorama() == true) {
return ValueListenableBuilder(
valueListenable: _enableFullScreenNotifier,
builder: (context, value, child) {
return IgnorePointer(
ignoring: value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: !value ? 1.0 : 0.0,
child: Align(
alignment: Alignment.center,
child: Tooltip(
message: S.of(context).panorama,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: const Color(0xAA252525),
fixedSize: const Size(44, 44),
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: Center(
child: Stack(
children: [
_buildPageView(),
ValueListenableBuilder(
builder: (BuildContext context, int selectedIndex, _) {
return FileBottomBar(
_files![selectedIndex],
_onEditFileRequested,
widget.config.mode == DetailPageMode.minimalistic &&
!isGuestView,
onFileRemoved: _onFileRemoved,
userID: Configuration.instance.getUserID(),
enableFullScreenNotifier:
InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
);
},
valueListenable: _selectedIndexNotifier,
),
ValueListenableBuilder(
valueListenable: _selectedIndexNotifier,
builder: (BuildContext context, int selectedIndex, _) {
if (_files![selectedIndex].isPanorama() == true) {
return ValueListenableBuilder(
valueListenable: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
builder: (context, value, child) {
return IgnorePointer(
ignoring: value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: !value ? 1.0 : 0.0,
child: Align(
alignment: Alignment.center,
child: Tooltip(
message: S.of(context).panorama,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: const Color(0xAA252525),
fixedSize: const Size(44, 44),
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
),
icon: const Icon(
Icons.threesixty,
color: Colors.white,
size: 26,
),
onPressed: () async {
await openPanoramaViewerPage(
_files![selectedIndex],
);
},
),
),
),
),
);
},
);
}
return const SizedBox();
},
),
],
);
},
);
}
return const SizedBox();
},
),
],
),
),
),
),
@@ -251,7 +256,7 @@ class _DetailPageState extends State<DetailPage> {
).ignore();
}
Widget _buildPageView(BuildContext context) {
Widget _buildPageView() {
return PageView.builder(
clipBehavior: Clip.none,
itemBuilder: (context, index) {
@@ -271,14 +276,17 @@ class _DetailPageState extends State<DetailPage> {
},
playbackCallback: (isPlaying) {
Future.delayed(Duration.zero, () {
_toggleFullScreen(shouldEnable: isPlaying);
InheritedDetailPageState.of(context)
.toggleFullScreen(shouldEnable: isPlaying);
});
},
backgroundDecoration: const BoxDecoration(color: Colors.black),
);
return GestureDetector(
onTap: () {
file.fileType != FileType.video ? _toggleFullScreen() : null;
file.fileType != FileType.video
? InheritedDetailPageState.of(context).toggleFullScreen()
: null;
},
child: fileContent,
);
@@ -313,26 +321,6 @@ class _DetailPageState extends State<DetailPage> {
return false;
}
void _toggleFullScreen({bool? shouldEnable}) {
if (shouldEnable != null) {
if (_enableFullScreenNotifier.value == shouldEnable) return;
}
_enableFullScreenNotifier.value = !_enableFullScreenNotifier.value;
if (_enableFullScreenNotifier.value) {
Future.delayed(const Duration(milliseconds: 200), () {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [],
);
});
} else {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.edgeToEdge,
overlays: SystemUiOverlay.values,
);
}
}
void _preloadFiles(int index) {
if (index > 0) {
preloadFile(_files![index - 1]);

View File

@@ -100,11 +100,6 @@ class FileBottomBarState extends State<FileBottomBar> {
),
onPressed: () async {
await _displayDetails(widget.file);
safeRefresh(); //to instantly show the new caption if keypad is closed after pressing 'done' - here the caption will be updated before the bottom sheet is closed
await Future.delayed(
const Duration(milliseconds: 500),
); //Waiting for some time till the caption gets updated in db if the user closes the bottom sheet without pressing 'done'
safeRefresh();
},
),
),

View File

@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import "package:photos/core/event_bus.dart";
import "package:photos/events/file_caption_updated_event.dart";
import "package:photos/generated/l10n.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/keyboard/keyboard_oveylay.dart';
import 'package:photos/ui/components/keyboard/keyboard_top_button.dart';
import "package:photos/ui/notification/toast.dart";
import 'package:photos/utils/magic_util.dart';
class FileCaptionReadyOnly extends StatelessWidget {
@@ -71,18 +74,19 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
@override
void initState() {
super.initState();
_focusNode.addListener(_focusNodeListener);
editedCaption = widget.file.caption;
if (editedCaption != null && editedCaption!.isNotEmpty) {
hintText = editedCaption!;
}
super.initState();
}
@override
void dispose() {
if (editedCaption != null) {
editFileCaption(null, widget.file, editedCaption!);
editFileCaption(null, widget.file, editedCaption!)
.then((isSuccess) => _onEditFileFinish(isSuccess));
}
_textController.dispose();
_focusNode.removeListener(_focusNodeListener);
@@ -148,7 +152,8 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
Future<void> _onDoneClick(BuildContext context) async {
if (editedCaption != null) {
final isSuccesful =
await editFileCaption(context, widget.file, editedCaption!);
await editFileCaption(context, widget.file, editedCaption!)
.then((isSuccess) => _onEditFileFinish(isSuccess));
if (isSuccesful) {
if (mounted) {
Navigator.pop(context);
@@ -185,4 +190,15 @@ class _FileCaptionWidgetState extends State<FileCaptionWidget> {
KeyboardOverlay.removeOverlay();
}
}
bool _onEditFileFinish(bool isSuccess) {
if (isSuccess) {
widget.file.pubMagicMetadata?.caption = editedCaption;
Bus.instance.fire(FileCaptionUpdatedEvent(widget.file.generatedID!));
return true;
} else {
showShortToast(context, S.of(context).somethingWentWrong);
return false;
}
}
}

View File

@@ -5,6 +5,7 @@ import "package:media_kit_video/media_kit_video.dart";
import "package:photos/models/file/file.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/actions/file/file_actions.dart";
import "package:photos/ui/common/loading_widget.dart";
import "package:photos/ui/viewer/file/preview_status_widget.dart";
import "package:photos/utils/standalone/date_time.dart";
@@ -44,6 +45,7 @@ class _VideoWidgetState extends State<VideoWidget> {
@override
void initState() {
super.initState();
_isPlayingStreamSubscription =
widget.controller.player.stream.playing.listen((isPlaying) {
if (isPlaying && !_isSeekingNotifier.value) {
@@ -55,7 +57,6 @@ class _VideoWidgetState extends State<VideoWidget> {
});
_isSeekingNotifier.addListener(isSeekingListener);
super.initState();
}
@override
@@ -131,27 +132,6 @@ class _VideoWidgetState extends State<VideoWidget> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
widget.file.caption != null
? Padding(
padding: const EdgeInsets.fromLTRB(
16,
12,
16,
8,
),
child: Text(
widget.file.caption!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: getEnteTextTheme(context)
.mini
.copyWith(
color: textBaseDark,
),
textAlign: TextAlign.center,
),
)
: const SizedBox.shrink(),
PreviewStatusWidget(
showControls: value,
file: widget.file,
@@ -161,6 +141,7 @@ class _VideoWidgetState extends State<VideoWidget> {
SeekBarAndDuration(
controller: widget.controller,
isSeekingNotifier: _isSeekingNotifier,
file: widget.file,
),
],
),
@@ -272,11 +253,13 @@ class _PlayPauseButtonState extends State<PlayPauseButtonMediaKit> {
class SeekBarAndDuration extends StatelessWidget {
final VideoController? controller;
final ValueNotifier<bool> isSeekingNotifier;
final EnteFile file;
const SeekBarAndDuration({
super.key,
required this.controller,
required this.isSeekingNotifier,
required this.file,
});
@override
@@ -302,46 +285,73 @@ class SeekBarAndDuration extends StatelessWidget {
width: 1,
),
),
child: Row(
child: Column(
children: [
StreamBuilder(
stream: controller?.player.stream.position,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text(
"0:00",
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
);
}
return Text(
secondsToDuration(snapshot.data!.inSeconds),
file.caption != null && file.caption!.isNotEmpty
? Padding(
padding: const EdgeInsets.fromLTRB(
0,
8,
0,
12,
),
child: GestureDetector(
onTap: () {
showDetailsSheet(context, file);
},
child: Text(
file.caption!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context)
.mini
.copyWith(color: textBaseDark),
),
),
)
: const SizedBox.shrink(),
Row(
children: [
StreamBuilder(
stream: controller?.player.stream.position,
builder: (context, snapshot) {
if (snapshot.data == null) {
return Text(
"0:00",
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
);
}
return Text(
secondsToDuration(snapshot.data!.inSeconds),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
);
},
),
Expanded(
child: SeekBar(
controller!,
isSeekingNotifier,
),
),
Text(
_secondsToDuration(
controller!.player.state.duration.inSeconds,
),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
);
},
),
Expanded(
child: SeekBar(
controller!,
isSeekingNotifier,
),
),
Text(
_secondsToDuration(
controller!.player.state.duration.inSeconds,
),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
),
],
),
],
),

View File

@@ -7,6 +7,7 @@ import "package:media_kit/media_kit.dart";
import "package:media_kit_video/media_kit_video.dart";
import "package:photos/core/constants.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/file_caption_updated_event.dart";
import "package:photos/events/guest_view_event.dart";
import "package:photos/events/pause_video_event.dart";
import "package:photos/events/stream_switched_event.dart";
@@ -60,6 +61,8 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
late final StreamSubscription<GuestViewEvent> _guestViewEventSubscription;
bool _isGuestView = false;
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
late final StreamSubscription<FileCaptionUpdatedEvent>
_captionUpdatedSubscription;
@override
void initState() {
@@ -84,6 +87,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
_isGuestView = event.isGuestView;
});
});
_streamSwitchedSubscription =
Bus.instance.on<StreamSwitchedEvent>().listen((event) {
if (event.type != PlayerType.mediaKit || !mounted) return;
@@ -93,6 +97,15 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
loadOriginal();
}
});
_captionUpdatedSubscription =
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
if (event.fileGeneratedID == widget.file.generatedID) {
if (mounted) {
setState(() {});
}
}
});
}
void loadPreview() {
@@ -147,6 +160,7 @@ class _VideoWidgetMediaKitNewState extends State<VideoWidgetMediaKitNew>
_progressNotifier.dispose();
WidgetsBinding.instance.removeObserver(this);
player.dispose();
_captionUpdatedSubscription.cancel();
super.dispose();
}

View File

@@ -8,6 +8,7 @@ import "package:logging/logging.dart";
import "package:native_video_player/native_video_player.dart";
import "package:photos/core/constants.dart";
import "package:photos/core/event_bus.dart";
import "package:photos/events/file_caption_updated_event.dart";
import "package:photos/events/guest_view_event.dart";
import "package:photos/events/pause_video_event.dart";
import "package:photos/events/seekbar_triggered_event.dart";
@@ -80,6 +81,8 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
final _elTooltipController = ElTooltipController();
StreamSubscription<PlaybackEvent>? _subscription;
StreamSubscription<StreamSwitchedEvent>? _streamSwitchedSubscription;
late final StreamSubscription<FileCaptionUpdatedEvent>
_captionUpdatedSubscription;
int position = 0;
@override
@@ -114,6 +117,15 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
loadOriginal(update: true);
}
});
_captionUpdatedSubscription =
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
if (event.fileGeneratedID == widget.file.generatedID) {
if (mounted) {
setState(() {});
}
}
});
}
Future<void> setVideoSource() async {
@@ -207,6 +219,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
_isSeeking.dispose();
_debouncer.cancelDebounceTimer();
_elTooltipController.dispose();
_captionUpdatedSubscription.cancel();
super.dispose();
}
@@ -357,6 +370,7 @@ class _VideoWidgetNativeState extends State<VideoWidgetNative>
showControls: _showControls,
isSeeking: _isSeeking,
position: position,
file: widget.file,
)
: const SizedBox();
},
@@ -644,6 +658,7 @@ class _SeekBarAndDuration extends StatelessWidget {
final ValueNotifier<bool> showControls;
final ValueNotifier<bool> isSeeking;
final int position;
final EnteFile file;
const _SeekBarAndDuration({
required this.controller,
@@ -651,6 +666,7 @@ class _SeekBarAndDuration extends StatelessWidget {
required this.showControls,
required this.isSeeking,
required this.position,
required this.file,
});
@override
@@ -691,34 +707,61 @@ class _SeekBarAndDuration extends StatelessWidget {
width: 1,
),
),
child: Row(
child: Column(
children: [
AnimatedSize(
duration: const Duration(
seconds: 5,
),
curve: Curves.easeInOut,
child: Text(
secondsToDuration(position ~/ 1000),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
file.caption != null && file.caption!.isNotEmpty
? Padding(
padding: const EdgeInsets.fromLTRB(
0,
8,
0,
12,
),
),
),
Expanded(
child: SeekBar(
controller!,
durationToSeconds(duration),
isSeeking,
),
),
Text(
duration ?? "0:00",
style: getEnteTextTheme(context).mini.copyWith(
color: textBaseDark,
child: GestureDetector(
onTap: () {
showDetailsSheet(context, file);
},
child: Text(
file.caption!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context)
.mini
.copyWith(color: textBaseDark),
),
),
)
: const SizedBox.shrink(),
Row(
children: [
AnimatedSize(
duration: const Duration(
seconds: 5,
),
curve: Curves.easeInOut,
child: Text(
secondsToDuration(position ~/ 1000),
style: getEnteTextTheme(
context,
).mini.copyWith(
color: textBaseDark,
),
),
),
Expanded(
child: SeekBar(
controller!,
durationToSeconds(duration),
isSeeking,
),
),
Text(
duration ?? "0:00",
style: getEnteTextTheme(context).mini.copyWith(
color: textBaseDark,
),
),
],
),
],
),
@@ -748,115 +791,85 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: Platform.isAndroid
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.center,
children: [
file.caption?.isNotEmpty ?? false
? Expanded(
child: ValueListenableBuilder(
valueListenable: showControls,
builder: (context, value, _) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInQuad,
opacity: value ? 1 : 0,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Text(
file.caption!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: getEnteTextTheme(context)
.mini
.copyWith(color: textBaseDark),
textAlign: TextAlign.center,
),
return Platform.isAndroid && !selectedPreview
? Align(
alignment: Alignment.centerRight,
child: ValueListenableBuilder(
valueListenable: showControls,
builder: (context, value, _) {
return IgnorePointer(
ignoring: !value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInQuad,
opacity: value ? 1 : 0,
child: ElTooltip(
padding: const EdgeInsets.all(12),
distance: 4,
controller: elTooltipController,
content: GestureDetector(
onLongPress: () {
Bus.instance.fire(
UseMediaKitForVideo(),
);
HapticFeedback.vibrate();
elTooltipController.hide();
},
child: Text(S.of(context).useDifferentPlayerInfo),
),
);
},
),
)
: const SizedBox.shrink(),
Platform.isAndroid && !selectedPreview
? ValueListenableBuilder(
valueListenable: showControls,
builder: (context, value, _) {
return IgnorePointer(
ignoring: !value,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInQuad,
opacity: value ? 1 : 0,
child: ElTooltip(
padding: const EdgeInsets.all(12),
distance: 4,
controller: elTooltipController,
content: GestureDetector(
onLongPress: () {
Bus.instance.fire(
UseMediaKitForVideo(),
);
HapticFeedback.vibrate();
position: ElTooltipPosition.topEnd,
color: backgroundElevatedDark,
appearAnimationDuration: const Duration(
milliseconds: 200,
),
disappearAnimationDuration: const Duration(
milliseconds: 200,
),
child: GestureDetector(
onTap: () {
if (elTooltipController.value ==
ElTooltipStatus.hidden) {
elTooltipController.show();
} else {
elTooltipController.hide();
},
child: Text(S.of(context).useDifferentPlayerInfo),
),
position: ElTooltipPosition.topEnd,
color: backgroundElevatedDark,
appearAnimationDuration: const Duration(
milliseconds: 200,
),
disappearAnimationDuration: const Duration(
milliseconds: 200,
),
child: GestureDetector(
onTap: () {
if (elTooltipController.value ==
ElTooltipStatus.hidden) {
elTooltipController.show();
} else {
elTooltipController.hide();
}
controller?.pause();
},
behavior: HitTestBehavior.translucent,
onLongPress: () {
Bus.instance.fire(
UseMediaKitForVideo(),
);
HapticFeedback.vibrate();
},
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
child: Container(
padding: const EdgeInsets.all(12),
child: Stack(
alignment: Alignment.bottomRight,
children: [
Icon(
Icons.play_arrow_outlined,
size: 24,
color: Colors.white.withOpacity(0.2),
),
Icon(
Icons.question_mark_rounded,
size: 10,
color: Colors.white.withOpacity(0.2),
),
],
),
}
controller?.pause();
},
behavior: HitTestBehavior.translucent,
onLongPress: () {
Bus.instance.fire(
UseMediaKitForVideo(),
);
HapticFeedback.vibrate();
},
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 0, 4),
child: Container(
padding: const EdgeInsets.all(12),
child: Stack(
alignment: Alignment.bottomRight,
children: [
Icon(
Icons.play_arrow_outlined,
size: 24,
color: Colors.white.withOpacity(0.2),
),
Icon(
Icons.question_mark_rounded,
size: 10,
color: Colors.white.withOpacity(0.2),
),
],
),
),
),
),
),
);
},
)
: const SizedBox.shrink(),
],
);
),
);
},
),
)
: const SizedBox.shrink();
}
}

View File

@@ -9,10 +9,14 @@ import 'package:photos/core/cache/thumbnail_in_memory_cache.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import "package:photos/events/file_caption_updated_event.dart";
import "package:photos/events/files_updated_event.dart";
import 'package:photos/events/local_photos_updated_event.dart';
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/states/detail_page_state.dart";
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import "package:photos/ui/actions/file/file_actions.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/utils/file_util.dart';
@@ -55,6 +59,8 @@ class _ZoomableImageState extends State<ZoomableImage> {
bool _isZooming = false;
PhotoViewController _photoViewController = PhotoViewController();
final _scaleStateController = PhotoViewScaleStateController();
late final StreamSubscription<FileCaptionUpdatedEvent>
_captionUpdatedSubscription;
@override
void initState() {
@@ -70,12 +76,22 @@ class _ZoomableImageState extends State<ZoomableImage> {
debugPrint("isZooming = $_isZooming, currentState $value");
// _logger.info('is reakky zooming $_isZooming with state $value');
};
_captionUpdatedSubscription =
Bus.instance.on<FileCaptionUpdatedEvent>().listen((event) {
if (event.fileGeneratedID == _photo.generatedID) {
if (mounted) {
setState(() {});
}
}
});
}
@override
void dispose() {
_photoViewController.dispose();
_scaleStateController.dispose();
_captionUpdatedSubscription.cancel();
super.dispose();
}
@@ -167,7 +183,68 @@ class _ZoomableImageState extends State<ZoomableImage> {
};
return GestureDetector(
onVerticalDragUpdate: verticalDragCallback,
child: content,
child: widget.photo.caption?.isNotEmpty ?? false
? Stack(
clipBehavior: Clip.none,
children: [
content,
Positioned(
bottom: 72 + MediaQuery.paddingOf(context).bottom,
left: 0,
right: 0,
child: ValueListenableBuilder<bool>(
valueListenable: InheritedDetailPageState.of(context)
.enableFullScreenNotifier,
builder: (context, doNotShowCaption, _) {
return AnimatedOpacity(
opacity: doNotShowCaption ? 0.0 : 1.0,
duration: const Duration(milliseconds: 200),
child: IgnorePointer(
ignoring: doNotShowCaption,
child: GestureDetector(
onTap: () {
showDetailsSheet(context, widget.photo);
},
child: Container(
color: Colors.black.withOpacity(0.1),
width: double.infinity,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 4.0,
horizontal: 8.0,
),
child: SizedBox(
width:
MediaQuery.sizeOf(context).width - 16,
child: Center(
child: Text(
widget.photo.caption!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: getEnteTextTheme(context)
.mini
.copyWith(
color: textBaseDark,
),
),
),
),
),
],
),
),
),
),
);
},
),
),
],
)
: content,
);
}

View File

@@ -134,37 +134,14 @@ class GalleryState extends State<Gallery> {
_reloadEventSubscription = widget.reloadEvent!.listen((event) async {
bool shouldReloadFromDB = true;
if (event.source == 'uploadCompleted') {
final Map<int, EnteFile> genIDToUploadedFiles = {};
for (int i = 0; i < event.updatedFiles.length; i++) {
if (event.updatedFiles[i].generatedID == null) {
shouldReloadFromDB = true;
break;
}
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
event.updatedFiles[i];
}
for (int i = 0; i < _allGalleryFiles.length; i++) {
final file = _allGalleryFiles[i];
if (file.generatedID == null) {
continue;
}
final updateFile = genIDToUploadedFiles[file.generatedID!];
if (updateFile != null &&
updateFile.localID == file.localID &&
areFromSameDay(
updateFile.creationTime ?? 0,
file.creationTime ?? 0,
)) {
_allGalleryFiles[i] = updateFile;
genIDToUploadedFiles.remove(file.generatedID!);
}
}
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
shouldReloadFromDB = _shouldReloadOnUploadCompleted(event);
} else if (event.source == 'fileMissingLocal') {
shouldReloadFromDB = _shouldReloadOnFileMissingLocal(event);
}
if (!shouldReloadFromDB) {
final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles);
_logger.info(
'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState',
'Skip softRefresh from DB on ${event.reason}, processed updated in memory with setStateReload $hasCalledSetState',
);
return;
}
@@ -231,6 +208,90 @@ class GalleryState extends State<Gallery> {
}
}
bool _shouldReloadOnUploadCompleted(FilesUpdatedEvent event) {
bool shouldReloadFromDB = true;
if (event.source == 'uploadCompleted') {
final Map<int, EnteFile> genIDToUploadedFiles = {};
for (int i = 0; i < event.updatedFiles.length; i++) {
// matching happens on generatedID and localID
if (event.updatedFiles[i].generatedID == null) {
return true;
}
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
event.updatedFiles[i];
}
for (int i = 0; i < _allGalleryFiles.length; i++) {
final file = _allGalleryFiles[i];
if (file.generatedID == null) {
continue;
}
final updateFile = genIDToUploadedFiles[file.generatedID!];
if (updateFile != null &&
updateFile.localID == file.localID &&
areFromSameDay(
updateFile.creationTime ?? 0,
file.creationTime ?? 0,
)) {
_allGalleryFiles[i] = updateFile;
genIDToUploadedFiles.remove(file.generatedID!);
}
}
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
}
return shouldReloadFromDB;
}
// Handle event when an local file was already uploaded and we have now
// added localID link link to the remote file
bool _shouldReloadOnFileMissingLocal(FilesUpdatedEvent event) {
bool shouldReloadFromDB = true;
if (event.source != 'fileMissingLocal' ||
event.type != EventType.deletedFromEverywhere) {
_logger.warning(
"Invalid event source or type for fileMissingLocal: ${event.source} ${event.type}",
);
return true;
}
final Map<int, EnteFile> genIDToUploadedFiles = {};
for (int i = 0; i < event.updatedFiles.length; i++) {
// the file should have generatedID, localID and should not be uploaded for
// following logic to work
if (event.updatedFiles[i].generatedID == null ||
event.updatedFiles[i].localID == null ||
event.updatedFiles[i].isUploaded) {
_logger.warning(
"Invalid file in updatedFiles: ${event.updatedFiles[i].localID} ${event.updatedFiles[i].generatedID} ${event.updatedFiles[i].isUploaded}",
);
return shouldReloadFromDB;
}
genIDToUploadedFiles[event.updatedFiles[i].generatedID!] =
event.updatedFiles[i];
}
final List<EnteFile> newAllGalleryFiles = [];
for (int i = 0; i < _allGalleryFiles.length; i++) {
final file = _allGalleryFiles[i];
if (file.generatedID == null) {
newAllGalleryFiles.add(file);
continue;
}
final updateFile = genIDToUploadedFiles[file.generatedID!];
if (updateFile != null &&
areFromSameDay(
updateFile.creationTime ?? 0,
file.creationTime ?? 0,
)) {
genIDToUploadedFiles.remove(file.generatedID!);
} else {
newAllGalleryFiles.add(file);
}
}
shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty;
if (!shouldReloadFromDB) {
_allGalleryFiles = newAllGalleryFiles;
}
return shouldReloadFromDB;
}
// group files into multiple groups and returns `true` if it resulted in a
// gallery reload
bool _onFilesLoaded(List<EnteFile> files) {

View File

@@ -11,6 +11,7 @@ import "package:photos/generated/l10n.dart";
import "package:photos/l10n/l10n.dart";
import 'package:photos/models/collection/collection.dart';
import "package:photos/models/selected_files.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/collections_service.dart";
import "package:photos/services/filter/db_filters.dart";
import "package:photos/theme/colors.dart";
@@ -25,7 +26,6 @@ import "package:photos/ui/components/title_bar_title_widget.dart";
import "package:photos/ui/viewer/gallery/gallery.dart";
import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart";
import "package:photos/utils/dialog_util.dart";
import "package:photos/utils/photo_manager_util.dart";
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
Future<dynamic> showAddPhotosSheet(
@@ -191,7 +191,8 @@ class AddPhotosPhotoWidget extends StatelessWidget {
}
} catch (e) {
if (e is StateError) {
final PermissionState ps = await requestPhotoMangerPermissions();
final PermissionState ps =
await permissionService.requestPhotoMangerPermissions();
if (ps != PermissionState.authorized && ps != PermissionState.limited) {
await showChoiceDialog(
context,

View File

@@ -14,7 +14,7 @@ Future<bool> isAndroidSDKVersionLowerThan(int inputSDK) async {
}
}
Future<String?> getDeviceName() async {
Future<String?> getDeviceInfo() async {
try {
if (Platform.isIOS) {
final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
@@ -22,7 +22,7 @@ Future<String?> getDeviceName() async {
} else if (Platform.isAndroid) {
final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
return "${androidInfo.brand} ${androidInfo.model}";
return "${androidInfo.brand} ${androidInfo.model} Android ${androidInfo.version.release}";
} else {
return "Not iOS or Android";
}

View File

@@ -237,11 +237,9 @@ Future<bool> editFileCaption(
caption,
showDoneToast: false,
);
return true;
} catch (e) {
if (context != null) {
showShortToast(context, S.of(context).somethingWentWrong);
}
return false;
}
}

View File

@@ -1,12 +0,0 @@
import "package:photo_manager/photo_manager.dart";
Future<PermissionState> requestPhotoMangerPermissions() {
return PhotoManager.requestPermissionExtend(
requestOption: const PermissionRequestOption(
androidPermission: AndroidPermission(
type: RequestType.common,
mediaLocation: true,
),
),
);
}

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.99+1012
version: 0.9.101+1014
publish_to: none
environment:

View File

@@ -42,7 +42,7 @@ func NewOfferController(
blackFridayOffers := make(ente.BlackFridayOfferPerCountry)
path, err := config.BillingConfigFilePath("black-friday.json")
if err != nil {
log.Fatalf("Error getting offer config file: %v", err)
log.Fatalf("Skipping BF configuration, config file not found: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {

View File

@@ -88,7 +88,7 @@ func parsePricingFile(fileName string) ente.BillingPlansPerCountry {
}
data, err := os.ReadFile(filePath)
if err != nil {
logrus.Errorf("Error reading file %s: %v\n", filePath, err)
logrus.Errorf("Skipping payment configuration, pricing data unavailable in config: %v\n", err)
return nil
}

View File

@@ -1 +0,0 @@
thirdparty/

View File

@@ -1,8 +1,15 @@
{
"tabWidth": 4,
"proseWrap": "always",
"objectWrap": "collapse",
"plugins": [
"prettier-plugin-organize-imports",
"prettier-plugin-packagejson"
],
"overrides": [
{
"files": ["packages/base/locales/**/*.json"],
"options": { "objectWrap": "preserve" }
}
]
}

View File

@@ -12,12 +12,6 @@ To know more about Ente, see [our main README](../README.md) or visit
## Building from source
Fetch submodules
```sh
git submodule update --init --recursive
```
Install dependencies
```sh

View File

@@ -127,12 +127,7 @@ const Page = () => {
return;
}
return {
redirectURL,
passkeySessionID,
clientPackage,
beginResponse,
};
return { redirectURL, passkeySessionID, clientPackage, beginResponse };
}, []);
/**

View File

@@ -139,9 +139,7 @@ interface BeginPasskeyRegistrationResponse {
* Options that should be passed to `navigator.credential.create` when
* creating the new {@link Credential}.
*/
options: {
publicKey: PublicKeyCredentialCreationOptions;
};
options: { publicKey: PublicKeyCredentialCreationOptions };
}
const beginPasskeyRegistration = async (token: string) => {
@@ -299,11 +297,7 @@ const finishPasskeyRegistration = async ({
// anyways for transmission, we can just reuse the same string.
rawId: credential.id,
type: credential.type,
response: {
attestationObject,
clientDataJSON,
transports,
},
response: { attestationObject, clientDataJSON, transports },
}),
});
ensureOk(res);
@@ -379,9 +373,7 @@ export interface BeginPasskeyAuthenticationResponse {
* Options that should be passed to `navigator.credential.get` to obtain the
* attested {@link Credential}.
*/
options: {
publicKey: PublicKeyCredentialRequestOptions;
};
options: { publicKey: PublicKeyCredentialRequestOptions };
}
/**

View File

@@ -1,17 +1,11 @@
[
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "photos-web",
"site": "https://web.ente.io"
}
"target": { "namespace": "photos-web", "site": "https://web.ente.io" }
},
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "auth-web",
"site": "https://auth.ente.io"
}
"target": { "namespace": "auth-web", "site": "https://auth.ente.io" }
},
{
"relation": ["delegate_permission/common.get_login_creds"],

View File

@@ -290,9 +290,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => {
}
case "steam": {
const steam = new Steam({
secret: code.secret,
});
const steam = new Steam({ secret: code.secret });
otp = steam.generate();
nextOTP = steam.generate({
timestamp: Date.now() + code.period * 1000,

View File

@@ -10,6 +10,6 @@
},
"devDependencies": {
"@/build-config": "*",
"@types/chromecast-caf-receiver": "^6.0.17"
"@types/chromecast-caf-receiver": "^6.0.21"
}
}

View File

@@ -110,9 +110,7 @@ const registerDevice = async (publicKey: string) => {
const res = await fetch(await apiURL("/cast/device-info"), {
method: "POST",
headers: publicRequestHeaders(),
body: JSON.stringify({
publicKey,
}),
body: JSON.stringify({ publicKey }),
});
ensureOk(res);
return z.object({ deviceCode: z.string() }).parse(await res.json())

View File

@@ -166,9 +166,7 @@ const getEncryptedCollectionFiles = async (
resp = await HTTPService.get(
await apiURL("/cast/diff"),
{ sinceTime },
{
"X-Cast-Access-Token": castToken,
},
{ "X-Cast-Access-Token": castToken },
);
const diff = resp.data.diff;
files = files.concat(diff.filter((file: EnteFile) => !file.isDeleted));
@@ -328,9 +326,7 @@ const downloadFile = async (
? `https://cast-albums.ente.io/preview/?fileID=${file.id}`
: `https://cast-albums.ente.io/download/?fileID=${file.id}`;
return fetch(url, {
headers: {
"X-Cast-Access-Token": castToken,
},
headers: { "X-Cast-Access-Token": castToken },
});
}
};

View File

@@ -15,9 +15,9 @@
},
"devDependencies": {
"@/build-config": "*",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.11"
"vite": "^6.2.1"
}
}

View File

@@ -79,11 +79,7 @@ const stripePublishableKey = (accountCountry: StripeAccountCountry) => {
/** Return the {@link StripeAccountCountry} for the user. */
const getUserStripeAccountCountry = async (paymentToken: string) => {
const url = `${apiOrigin}/billing/stripe-account-country`;
const res = await fetch(url, {
headers: {
"X-Auth-Token": paymentToken,
},
});
const res = await fetch(url, { headers: { "X-Auth-Token": paymentToken } });
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
const json: unknown = await res.json();
if (json && typeof json === "object" && "stripeAccountCountry" in json) {
@@ -138,11 +134,7 @@ const createCheckoutSession = async (
): Promise<string> => {
const params = new URLSearchParams({ productID, redirectURL });
const url = `${apiOrigin}/billing/stripe/checkout-session?${params.toString()}`;
const res = await fetch(url, {
headers: {
"X-Auth-Token": paymentToken,
},
});
const res = await fetch(url, { headers: { "X-Auth-Token": paymentToken } });
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
const json: unknown = await res.json();
if (json && typeof json == "object" && "sessionID" in json) {
@@ -230,12 +222,8 @@ async function updateStripeSubscription(
const url = `${apiOrigin}/billing/stripe/update-subscription`;
const res = await fetch(url, {
method: "POST",
headers: {
"X-Auth-Token": paymentToken,
},
body: JSON.stringify({
productID,
}),
headers: { "X-Auth-Token": paymentToken },
body: JSON.stringify({ productID }),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
const json: unknown = await res.json();

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