Compare commits

...

401 Commits

Author SHA1 Message Date
Neeraj Gupta
38503e8673 Merge remote-tracking branch 'origin/main' into remote_db 2025-09-02 10:34:47 +05:30
Neeraj Gupta
c5319f2ba8 Merge remote-tracking branch 'origin/main' into remote_db 2025-08-21 17:19:42 +05:30
Neeraj Gupta
5d851d8f90 Add model for localAsset 2025-08-08 16:11:52 +05:30
Neeraj Gupta
dd76317bb0 Clean up EnteFile 2025-08-07 15:45:55 +05:30
Neeraj Gupta
5519981f9c Stop reading redundant data 2025-08-07 15:43:55 +05:30
Neeraj Gupta
1c249560c0 Clean up EnteFile 2025-08-07 15:10:56 +05:30
Neeraj Gupta
130cc9137c File:Duration Switch to get only prop 2025-08-07 13:53:29 +05:30
Neeraj Gupta
95f2d282b5 Clean up EnteFile 2025-08-07 13:51:04 +05:30
Neeraj Gupta
e6668606db Minimize data read 2025-08-07 13:37:43 +05:30
Neeraj Gupta
316c767ffa Merge branch 'reduceLog' into remote_db 2025-08-07 11:32:25 +05:30
Neeraj Gupta
c08b0f7aa4 Build changes 2025-08-07 11:08:15 +05:30
Neeraj Gupta
b5a4bcf98f Clean up EnteFile 2025-08-07 11:08:03 +05:30
Neeraj Gupta
dc06a2e193 Use getter for title 2025-08-07 10:41:20 +05:30
Neeraj Gupta
c29446e00a Add index 2025-08-06 14:51:41 +05:30
Neeraj Gupta
5aa36921d8 Use remoteDB 2025-08-06 12:38:06 +05:30
Neeraj Gupta
3b3af65d74 Rewrite reload and cache clear check 2025-08-06 11:35:12 +05:30
Neeraj Gupta
9ad88d3908 Filter out files for which we don't need to create streams 2025-08-06 10:50:34 +05:30
Neeraj Gupta
012e9091f0 Use remote DB 2025-08-06 10:34:43 +05:30
Neeraj Gupta
8ff0f237e7 Use remote DB 2025-08-06 10:17:27 +05:30
Neeraj Gupta
c80e4a65b8 Set state context early 2025-08-06 09:14:08 +05:30
Neeraj Gupta
c317f2494f Fix parsing 2025-08-06 09:13:42 +05:30
Neeraj Gupta
264b0b151a Merge remote-tracking branch 'origin/main' into remote_db 2025-08-05 14:41:02 +05:30
Neeraj Gupta
e5cb3e7005 Hide uploaded local files from homescreen 2025-08-05 14:31:38 +05:30
Neeraj Gupta
adc1939638 Use existing method 2025-08-05 13:15:12 +05:30
Neeraj Gupta
2cc1c36b7b Flatten all remote asset fields 2025-08-05 13:12:34 +05:30
Neeraj Gupta
b99450615e Remove unused field 2025-08-05 09:35:02 +05:30
Neeraj Gupta
5de1f0c93b Switch to remoteDB 2025-07-31 16:24:23 +05:30
Neeraj Gupta
ee902a5ccb ML typecast fixes 2025-07-31 15:39:53 +05:30
Neeraj Gupta
9e56afaa73 Fixed query 2025-07-31 15:35:48 +05:30
Neeraj Gupta
77f8c3f712 Fix: Specify type 2025-07-31 15:04:43 +05:30
Neeraj Gupta
f7a0a414db Fix: Use new db to get fileKey 2025-07-31 15:02:25 +05:30
Neeraj Gupta
bd49b8464b Minor db query fix 2025-07-31 14:20:55 +05:30
Neeraj Gupta
0cbfa319ba Use remote db for dedupe 2025-07-31 14:15:25 +05:30
Neeraj Gupta
95e05f167c Merge remote-tracking branch 'origin/main' into remote_db 2025-07-31 12:07:04 +05:30
Neeraj Gupta
84ef4c2d0b Clean up 2025-07-31 11:22:24 +05:30
Neeraj Gupta
53c14dff01 Refactor 2025-07-31 10:34:42 +05:30
Neeraj Gupta
02ef29fd8f Consolidate shared assets deletion logic 2025-07-31 10:29:00 +05:30
Neeraj Gupta
1848f1a94b Use remoteDB 2025-07-29 15:14:36 +05:30
Neeraj Gupta
276c38236f Use new db to identify backed up files 2025-07-29 14:53:26 +05:30
Neeraj Gupta
5e3e3b4427 Clean up 2025-07-28 16:53:57 +05:30
Neeraj Gupta
b8dab3ea1c Use new db for archive file count 2025-07-28 14:57:36 +05:30
Neeraj Gupta
2dc32f5339 Strip out exif from metadata 2025-07-28 14:54:40 +05:30
Neeraj Gupta
9fd6bc4974 Fix query 2025-07-28 14:54:24 +05:30
Neeraj Gupta
3740e9e29d Gracefully handle partial cleanup 2025-07-28 14:33:34 +05:30
Neeraj Gupta
118dde38a2 Refactor 2025-07-28 11:48:01 +05:30
Neeraj Gupta
cd15fe86c6 Fix delete 2025-07-28 11:47:55 +05:30
Neeraj Gupta
693a40cc24 Refactor save asset for edits 2025-07-26 12:17:53 +05:30
Neeraj Gupta
2cfa8497da Merge remote-tracking branch 'origin/main' into remote_db 2025-07-26 11:53:48 +05:30
Neeraj Gupta
3f6b9d7ae6 Refactor 2025-07-26 11:13:54 +05:30
Neeraj Gupta
e71b6dbb63 Remove unused method 2025-07-26 11:04:49 +05:30
Neeraj Gupta
44722c40c2 Merge branch 'main' into remote_db 2025-07-24 16:22:57 +05:30
Neeraj Gupta
bfd13b99d2 Fix compile error 2025-07-21 11:36:59 +05:30
Neeraj Gupta
e82d878fa8 Merge branch 'main' into remote_db 2025-07-21 10:46:57 +05:30
Neeraj Gupta
0587813c70 Rename 2025-07-14 14:43:08 +05:30
Neeraj Gupta
b7712a7c51 Clean up 2025-07-14 14:42:16 +05:30
Neeraj Gupta
213f3cd122 Clean up 2025-07-14 11:44:13 +05:30
Neeraj Gupta
d103274da5 Add file matching logic before upload 2025-07-14 11:04:44 +05:30
Neeraj Gupta
28b244ebc5 Use fk to keep updated mapping info 2025-07-14 09:53:59 +05:30
Neeraj Gupta
7d24dea7bc Merge branch 'main' into remote_db 2025-07-11 16:56:34 +05:30
Neeraj Gupta
4a358a7793 Refactor 2025-07-11 11:09:00 +05:30
Neeraj Gupta
699249cd26 Upload [1/x] 2025-07-11 10:15:35 +05:30
Neeraj Gupta
7125ac7419 Simplify 2025-07-10 10:34:50 +05:30
Neeraj Gupta
de67b6e9fc Rename column and minor fixes 2025-07-09 15:20:49 +05:30
Neeraj Gupta
39868de5d9 Update schema for shared assets 2025-07-09 14:22:33 +05:30
Neeraj Gupta
751550d469 Refactor 2025-07-09 12:01:46 +05:30
Neeraj Gupta
6f0034dd9d Merge remote-tracking branch 'origin/main' into remote_db 2025-07-09 10:07:55 +05:30
Neeraj Gupta
7bd6180ebf Reflect backup status for device folder 2025-07-09 10:06:25 +05:30
Neeraj Gupta
5a3ae5f97c Log stack trace 2025-07-09 10:05:45 +05:30
Neeraj Gupta
e447058573 Simplify 2025-07-09 09:08:21 +05:30
Neeraj Gupta
8a7e8b8237 Fix queue clean up on backup folder update 2025-07-08 17:57:38 +05:30
Neeraj Gupta
ed7b1be591 Re-organize 2025-07-08 15:08:56 +05:30
Neeraj Gupta
9a5ef8b634 Clean up 2025-07-08 11:29:37 +05:30
Neeraj Gupta
c2668387b5 Refactor to use new upload media and metadata 2025-07-08 11:01:37 +05:30
Neeraj Gupta
d265cf62c7 Separate model to get upload media 2025-07-08 09:18:57 +05:30
Neeraj Gupta
a67c3b0624 Clean up fields 2025-07-07 16:57:06 +05:30
Neeraj Gupta
306c78fbb0 Refactor 2025-07-07 16:09:40 +05:30
Neeraj Gupta
e4f9dd6b33 Simplify new files notification + avoid redundant compute 2025-07-07 15:57:47 +05:30
Neeraj Gupta
5fec59f0fe Clean up 2025-07-07 15:20:32 +05:30
Neeraj Gupta
53050ca25e Map remote files to local during diff sync 2025-07-07 14:31:37 +05:30
Neeraj Gupta
926aa42168 Merge branch 'main' into remote_db 2025-07-07 09:52:59 +05:30
Neeraj Gupta
f4f8141b99 Move mobile -> mobile/apps/photos 2025-07-04 16:52:07 +05:30
Neeraj Gupta
3d044dd3d4 Refactor metadata computation 2025-07-04 14:06:11 +05:30
Neeraj Gupta
8699ad2f01 New upload media data model 2025-07-04 11:52:27 +05:30
Neeraj Gupta
01aad531f1 Generated changes 2025-07-04 10:05:42 +05:30
Neeraj Gupta
6340d9f646 Simplify 2025-07-04 09:37:17 +05:30
Neeraj Gupta
0f944b1796 Simplify 2025-07-04 09:33:02 +05:30
Neeraj Gupta
1d8533168f Mark fields non-nullable 2025-07-03 17:32:58 +05:30
Neeraj Gupta
2fca1ba534 Add location in shared assets 2025-07-03 16:29:38 +05:30
Neeraj Gupta
2d386c769b Simplify 2025-07-03 11:41:14 +05:30
Neeraj Gupta
1b41d81839 Refactor 2025-07-02 20:08:49 +05:30
Neeraj Gupta
487156c7df Use computer for parsing XMP 2025-07-02 19:32:19 +05:30
Neeraj Gupta
35c54111e7 Remove unused import 2025-07-02 17:24:55 +05:30
Neeraj Gupta
d71340fbdd Refactor 2025-07-02 17:19:35 +05:30
Neeraj Gupta
f32ea85ee2 Refactor 2025-07-02 16:37:06 +05:30
Neeraj Gupta
6397ab888a Merge branch 'main' into remote_db 2025-07-02 14:48:53 +05:30
Neeraj Gupta
5a73043b63 Measure time 2025-07-02 14:13:39 +05:30
Neeraj Gupta
f258c40e98 Measure time 2025-07-02 14:13:20 +05:30
Neeraj Gupta
36b6476049 Use remote DB 2025-07-02 11:32:47 +05:30
Neeraj Gupta
c4c5ea150f Merge branch 'main' into remote_db 2025-07-02 09:30:08 +05:30
Neeraj Gupta
1956b3788b Rename 2025-07-01 15:52:11 +05:30
Neeraj Gupta
72cbddff6d Use upload queue for pending uploads 2025-07-01 15:42:48 +05:30
Neeraj Gupta
17670d5538 Use asset queue db for queueing pending upload 2025-07-01 15:25:52 +05:30
Neeraj Gupta
8cfd80663e Merge branch 'main' into remote_db 2025-07-01 14:55:11 +05:30
Neeraj Gupta
f285e2d706 Use lAsset for video type 2025-06-30 17:49:15 +05:30
Neeraj Gupta
f60e074dd5 Use tag instead of generatedID 2025-06-30 17:00:45 +05:30
Neeraj Gupta
140eae6859 Use tag instead of genID 2025-06-30 16:52:23 +05:30
Neeraj Gupta
23ee022472 Refactor 2025-06-30 16:46:08 +05:30
Neeraj Gupta
b1cf3f9fb0 Refactor live photo 2025-06-30 15:50:46 +05:30
Neeraj Gupta
dbbb80a817 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-30 14:35:46 +05:30
Neeraj Gupta
91a10634cc Remove redundant import 2025-06-30 13:01:53 +05:30
Neeraj Gupta
0a2c230254 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-30 12:24:18 +05:30
Neeraj Gupta
6745b110df Refactor shared assets 2025-06-30 12:23:01 +05:30
Neeraj Gupta
7061161181 Rename 2025-06-30 11:54:41 +05:30
Neeraj Gupta
f0026f0a81 Refactor 2025-06-30 11:52:36 +05:30
Neeraj Gupta
acec985bcb Add shared asset service 2025-06-30 11:31:42 +05:30
Neeraj Gupta
c8103a9e06 Refactor 2025-06-28 13:54:16 +05:30
Neeraj Gupta
02f64ad45f Refactor 2025-06-27 15:32:19 +05:30
Neeraj Gupta
d0931d1d0e Refactor 2025-06-27 15:17:08 +05:30
Neeraj Gupta
5c78de5355 Refactor 2025-06-27 14:08:47 +05:30
Neeraj Gupta
1aa9f61419 Rename 2025-06-27 12:49:00 +05:30
Neeraj Gupta
0f2b51d1a5 Remove redundant method 2025-06-27 12:46:29 +05:30
Neeraj Gupta
9fef560d15 Refactor 2025-06-27 12:02:01 +05:30
Neeraj Gupta
09c7bfd717 refactor 2025-06-26 14:44:44 +05:30
Neeraj Gupta
2ff059a701 Remove unused file 2025-06-26 14:25:05 +05:30
Neeraj Gupta
70b043d34a Migrate restore to new store 2025-06-26 14:19:15 +05:30
Neeraj Gupta
15a00379b5 Use remoteDB for linking 2025-06-26 10:01:25 +05:30
Neeraj Gupta
7246ade2ae Rename 2025-06-26 09:50:42 +05:30
Neeraj Gupta
7cad0a83d2 Clean up 2025-06-26 00:04:00 +05:30
Neeraj Gupta
e6703aef65 Simplify 2025-06-25 23:38:03 +05:30
Neeraj Gupta
662dfad7ca Remove unused method 2025-06-25 23:20:18 +05:30
Neeraj Gupta
466ab30f8b Improve handling of empty collection for cover 2025-06-25 22:55:04 +05:30
Neeraj Gupta
12c1845a5f Clean up 2025-06-25 22:43:44 +05:30
Neeraj Gupta
2d0202df36 Refactor magic metadata 2025-06-25 22:24:17 +05:30
Neeraj Gupta
2ec4f5a7e5 Use rAsset for metadata 2025-06-25 21:55:14 +05:30
Neeraj Gupta
6723bed1b0 Refactor filemagic service to use rAsset 2025-06-25 17:22:09 +05:30
Neeraj Gupta
84e8ce519e Fix error parsing for internal dialog 2025-06-25 16:43:05 +05:30
Neeraj Gupta
1b50528181 Remove unused filed 2025-06-25 14:58:23 +05:30
Neeraj Gupta
08dff77ad4 Refactor 2025-06-25 14:58:11 +05:30
Neeraj Gupta
713abce89a Refactor: get size from remoteAsset 2025-06-25 13:34:17 +05:30
Neeraj Gupta
da15593a47 Remove unused method 2025-06-25 12:44:40 +05:30
Neeraj Gupta
fc31cc61d1 Avoid redundant async 2025-06-25 12:44:34 +05:30
Neeraj Gupta
9c6259b713 Use remote db for favroite cache 2025-06-25 12:29:50 +05:30
Neeraj Gupta
ed603232a5 Merge branch 'main' into remote_db 2025-06-25 12:29:27 +05:30
Neeraj Gupta
ccd89d3451 Remove unused method 2025-06-25 11:28:28 +05:30
Neeraj Gupta
647b2ef4a7 Cache remoteAssets entry 2025-06-25 11:07:50 +05:30
Neeraj Gupta
f54c79462e Clear remote db on logout 2025-06-25 11:06:10 +05:30
Neeraj Gupta
eb81d96ddf Fix incorrect request key 2025-06-25 11:05:44 +05:30
Neeraj Gupta
57363a24ef Fix minor bugs 2025-06-25 09:23:34 +05:30
Neeraj Gupta
9431995e8c Clean up 2025-06-24 14:35:06 +05:30
Neeraj Gupta
64b86376f6 Cleanup 2025-06-24 13:31:58 +05:30
Neeraj Gupta
a825367c49 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-24 12:42:22 +05:30
Neeraj Gupta
1ffbb27ac5 Fix schema 2025-06-24 12:42:07 +05:30
Neeraj Gupta
d4add9f7ef Fix 2025-06-24 12:41:54 +05:30
Neeraj Gupta
541494613f Use CF for file key derivation 2025-06-24 12:19:55 +05:30
Neeraj Gupta
2b3427e40b Track shared assets separately 2025-06-24 11:55:28 +05:30
Neeraj Gupta
a57c9e881d Use remote db 2025-06-24 09:49:51 +05:30
Neeraj Gupta
d15f1e15ce Use remote db for file copy 2025-06-23 16:30:26 +05:30
Neeraj Gupta
0411f8ad40 Clean up 2025-06-23 15:04:42 +05:30
Neeraj Gupta
2981816c90 Use remotedb for trash 2025-06-23 14:54:48 +05:30
Neeraj Gupta
a6de98ef68 Add trash time 2025-06-23 14:35:13 +05:30
Neeraj Gupta
18156ce8bc Add trash group type 2025-06-23 14:24:56 +05:30
Neeraj Gupta
458c1cf86d Remove log 2025-06-21 11:28:05 +05:30
Neeraj Gupta
90c0874608 Merge branch 'main' into remote_db 2025-06-21 10:13:22 +05:30
Neeraj Gupta
928ffba4d7 rename 2025-06-21 09:14:01 +05:30
Neeraj Gupta
0701212540 Clean up 2025-06-21 08:46:49 +05:30
Neeraj Gupta
347bf4d2e0 Move 2025-06-21 07:57:41 +05:30
Neeraj Gupta
2729edfded rename 2025-06-21 07:57:26 +05:30
Neeraj Gupta
4e8d2c5cea Use local or remote id for selection 2025-06-21 07:56:55 +05:30
Neeraj Gupta
84e9336672 Use new abstraction to fetch public files 2025-06-20 14:22:02 +05:30
Neeraj Gupta
cecdea3f93 Rename 2025-06-20 13:21:14 +05:30
Neeraj Gupta
37674deba0 Implement move 2025-06-20 13:02:42 +05:30
Neeraj Gupta
733be57df8 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-19 22:49:29 +05:30
Neeraj Gupta
74df52baf1 rename 2025-06-19 16:28:35 +05:30
Neeraj Gupta
b817c4475e Refactor 2025-06-19 16:09:22 +05:30
Neeraj Gupta
f95dac31d2 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-19 11:44:20 +05:30
Neeraj Gupta
151289b24a Use remoteDB during add to collection 2025-06-17 17:24:16 +05:30
Neeraj Gupta
5dac9d4dd6 Rename 2025-06-17 16:44:13 +05:30
Neeraj Gupta
43b9dbdc54 Refactor 2025-06-17 16:14:56 +05:30
Neeraj Gupta
9d3caaa5d5 Use remoteDB to get files within duration 2025-06-17 15:45:13 +05:30
Neeraj Gupta
7eda2ed24e remove unused import 2025-06-17 14:24:23 +05:30
Neeraj Gupta
30df5271b4 Fix query 2025-06-17 14:24:12 +05:30
Neeraj Gupta
4b1f7612a3 Rename 2025-06-17 14:15:40 +05:30
Neeraj Gupta
bf6521e8d5 Use new remoteDB 2025-06-17 14:00:50 +05:30
Neeraj Gupta
4bac1bcb1d Merge remote-tracking branch 'origin/main' into remote_db 2025-06-17 13:34:12 +05:30
Neeraj Gupta
b123635584 Merge remote-tracking branch 'origin/main' into remote_db 2025-06-17 12:33:29 +05:30
Neeraj Gupta
d815143bb4 Rewrite upload queue logic 1/x 2025-06-16 14:02:12 +05:30
Neeraj Gupta
ff6228497f Add tables to support shared media & upload tracking 2025-06-11 17:02:03 +05:30
Neeraj Gupta
7469578e77 Merge branch 'main' into remote_db 2025-06-11 12:38:31 +05:30
Neeraj Gupta
76afef6149 Support for persiting path backup config 2025-06-10 15:58:18 +05:30
Neeraj Gupta
2b3178495a Fix build 2025-06-09 21:54:46 +05:30
Neeraj Gupta
d6f3ff8db3 Merge branch 'main' into remote_db 2025-06-09 13:15:33 +05:30
Neeraj Gupta
7b0ef2b0c0 Add table to store path backup config 2025-06-03 15:52:36 +05:30
Neeraj Gupta
35f95010ea Merge branch 'main' into remote_db 2025-06-03 12:46:53 +05:30
Neeraj Gupta
233f0ec1e1 Fix shared collections decryption 2025-05-28 16:36:46 +05:30
Neeraj Gupta
64820ff5fa Merge branch 'main' into remote_db 2025-05-28 16:36:30 +05:30
Neeraj Gupta
86ffd4e1e6 Merge branch 'main' into remote_db 2025-05-26 16:27:39 +05:30
Neeraj Gupta
436a02d352 Merge branch 'main' into remote_db 2025-05-19 15:34:32 +05:30
Neeraj Gupta
0f3b8bae48 Add models for upload mapping 2025-05-09 15:12:25 +05:30
Neeraj Gupta
0f270a379f Rename 2025-05-08 16:57:10 +05:30
Neeraj Gupta
609f6b8e18 Merge branch 'main' into remote_db 2025-05-08 15:28:06 +05:30
Neeraj Gupta
7896b397c2 Merge branch 'main' into remote_db 2025-05-08 13:08:08 +05:30
Neeraj Gupta
097078bd24 Switch to remoteDB 2025-05-02 16:34:11 +05:30
Neeraj Gupta
e43e3c4230 Switch to remoteDB 2025-05-02 16:26:18 +05:30
Neeraj Gupta
439f1ff0fb keep reference to collectionFileEntry 2025-05-02 16:09:44 +05:30
Neeraj Gupta
e5d78cfd99 Remove unused method 2025-05-02 16:09:26 +05:30
Neeraj Gupta
bd76e66abf perf refactor 2025-05-02 15:41:58 +05:30
Neeraj Gupta
9e6d7908a9 Refactor 2025-05-02 15:14:58 +05:30
Neeraj Gupta
281735e172 Show hidden files from latestDB 2025-05-02 12:28:18 +05:30
Neeraj Gupta
bc93aca110 Merge remote-tracking branch 'origin/main' into remote_db 2025-05-02 11:00:02 +05:30
Neeraj Gupta
df6e409ca1 Remove debugCaption 2025-04-30 15:52:35 +05:30
Neeraj Gupta
7489821434 Refactor 2025-04-30 15:50:31 +05:30
Neeraj Gupta
52e0f04ec2 Merge remote-tracking branch 'origin/main' into remote_db 2025-04-30 15:06:51 +05:30
Neeraj Gupta
ad88ce632c Remove unused method 2025-04-30 14:32:22 +05:30
Neeraj Gupta
26222ec836 Use remoteDB 2025-04-30 14:10:04 +05:30
Neeraj Gupta
beeaee4fd9 Switch to remote db 1/51 2025-04-30 13:18:17 +05:30
Neeraj Gupta
dc98c7bcf5 Remove unused method 2025-04-30 13:11:19 +05:30
Neeraj Gupta
82497563c2 Switch to remoteDB 2025-04-30 13:03:58 +05:30
Neeraj Gupta
4a9b4520d2 Rename 2025-04-30 12:25:52 +05:30
Neeraj Gupta
756c8e5b7d rename 2025-04-30 12:14:53 +05:30
Neeraj Gupta
dffb920cab Switch to remoteDB 2025-04-30 12:12:34 +05:30
Neeraj Gupta
fa7ddbba0c Merge branch 'main' into remote_db 2025-04-30 11:20:44 +05:30
Neeraj Gupta
13a068969c Catch uncaught exception to avoid splash screen issue 2025-04-30 11:10:18 +05:30
Neeraj Gupta
717e8c8b7e Disable iOS battery check in debugmode 2025-04-30 11:09:59 +05:30
Neeraj Gupta
015adb595c Support for DB filters on home screen 2025-04-30 10:37:53 +05:30
Neeraj Gupta
91635d2e7d Schema update 2025-04-30 10:32:10 +05:30
Neeraj Gupta
cd3499a004 Merge branch 'main' into remote_db 2025-04-30 09:27:09 +05:30
Neeraj Gupta
92a964cda6 Remove unused method 2025-04-28 15:47:00 +05:30
Neeraj Gupta
a9ba615962 Switch to new remotedb 2025-04-28 15:36:43 +05:30
Neeraj Gupta
f2b0c11622 Merge remote-tracking branch 'origin/main' into remote_db 2025-04-28 13:55:46 +05:30
Neeraj Gupta
83b89b6bbf Merge branch 'main' into remote_db 2025-04-28 13:55:26 +05:30
Neeraj Gupta
ee0a858302 Merge branch 'main' into remote_db 2025-04-28 09:52:10 +05:30
Neeraj Gupta
b34c923a66 merge files for home screen 2025-04-25 13:04:30 +05:30
Neeraj Gupta
fd927d038b Fix minor bugs 2025-04-25 12:36:57 +05:30
Neeraj Gupta
64e9902f57 Model & schema for local mapping 2025-04-25 12:36:33 +05:30
Neeraj Gupta
c3af79d113 Merge branch 'main' into remote_db 2025-04-24 08:50:49 +05:30
Neeraj Gupta
d87e679650 use remoteDB for collection cover 2025-04-23 12:00:21 +05:30
Neeraj Gupta
7e2242dc69 Show collection files 2025-04-22 16:34:47 +05:30
Neeraj Gupta
9adc207b02 Gradually load on device files 2025-04-22 15:10:09 +05:30
Neeraj Gupta
36049f6633 Treat default 0.0,0.0 as missing location 2025-04-22 14:14:32 +05:30
Neeraj Gupta
9341bc95ee Clean up 2025-04-22 13:57:38 +05:30
Neeraj Gupta
252dca1a01 Clean up 2025-04-22 13:07:53 +05:30
Neeraj Gupta
70501054d2 use remoteDb 2025-04-22 12:50:14 +05:30
Neeraj Gupta
be012e0a28 use remoteDb 2025-04-22 12:18:13 +05:30
Neeraj Gupta
340a0c097f Remove unused method 2025-04-22 12:11:20 +05:30
Neeraj Gupta
7fc8649455 use remoteDb 2025-04-22 11:58:56 +05:30
Neeraj Gupta
c97a313edb use remoteDb 2025-04-22 11:52:34 +05:30
Neeraj Gupta
480fdc84dc Merge remote-tracking branch 'origin/main' into remote_db 2025-04-22 11:52:10 +05:30
Neeraj Gupta
c92ef45c9a Refactor 2025-04-21 16:31:39 +05:30
Neeraj Gupta
ca62012a6f Store local mapping & creation time for remote files 2025-04-21 15:04:42 +05:30
Neeraj Gupta
151a0d13a4 Sync remote assets to local 2025-04-18 14:00:14 +05:30
Neeraj Gupta
747b1b84c6 Upsert instead of replace 2025-04-18 13:59:15 +05:30
Neeraj Gupta
e060fb9823 Disable metadata and ml indexing 2025-04-18 12:14:39 +05:30
Neeraj Gupta
cd377149bc Fix parsing 2025-04-18 11:48:23 +05:30
Neeraj Gupta
d9e22a489b Fix count query 2025-04-18 11:47:34 +05:30
Neeraj Gupta
524db74bf5 Read collection file count 2025-04-17 16:45:36 +05:30
Neeraj Gupta
e1222d51a9 Merge branch 'main' into remote_db 2025-04-17 16:17:36 +05:30
Neeraj Gupta
1c68f0bb60 Refactor 2025-04-17 15:30:40 +05:30
Neeraj Gupta
f6419caf5c Disable metadata scan on ios 2025-04-17 13:50:48 +05:30
Neeraj Gupta
441bcbd187 todo 2025-04-17 13:35:07 +05:30
Neeraj Gupta
4ad3927348 Add trigger to clean up stale collection entries 2025-04-17 13:34:51 +05:30
Neeraj Gupta
4ae15e5966 Remove unused import 2025-04-17 12:17:33 +05:30
Neeraj Gupta
d67d1d3df8 Index local files 2025-04-16 15:57:08 +05:30
Neeraj Gupta
07e1d33ca8 Merge branch 'main' into remote_db 2025-04-16 10:45:51 +05:30
Neeraj Gupta
50e15fa56c offline indexing 1/x 2025-04-15 16:03:39 +05:30
Neeraj Gupta
3f262c5ba2 Add db for offline ml data 2025-04-15 15:40:27 +05:30
Neeraj Gupta
7f34870e3a Remove unused code 2025-04-15 15:39:56 +05:30
Neeraj Gupta
8ba5013926 Merge branch 'main' into remote_db 2025-04-15 11:12:12 +05:30
Neeraj Gupta
e9a24efecb Make ml related classes generic 2025-04-14 16:23:26 +05:30
Neeraj Gupta
eaf74e4059 Merge branch 'main' into remote_db 2025-04-14 10:44:22 +05:30
Neeraj Gupta
e9e1c3ca27 Add wrapper for local metadata 2025-04-12 12:51:45 +05:30
Neeraj Gupta
eb34533aed Add enum for processing state 2025-03-29 13:42:57 +05:30
Neeraj Gupta
cd042e741e Merge branch 'main' into remote_db 2025-03-27 16:43:32 +05:30
Neeraj Gupta
6e944b0b55 Process latest assets first 2025-03-27 13:56:47 +05:30
Neeraj Gupta
18b6b499dd Store time in microseconds 2025-03-27 13:51:34 +05:30
Neeraj Gupta
9205ef8219 Fix: droid metadata parsing 2025-03-27 13:50:59 +05:30
Neeraj Gupta
1ecdbdb88e Fix: get device album assets in desc order 2025-03-27 13:42:18 +05:30
Neeraj Gupta
e2bb4d723e Perform local medatascan for droid 2025-03-27 10:46:33 +05:30
Neeraj Gupta
cfe32c47f0 Merge branch 'main' into remote_db 2025-03-27 06:34:35 +05:30
Neeraj Gupta
a7f6b6589d Service for parsing local metadata 2025-03-26 22:11:59 +05:30
Neeraj Gupta
715e305e09 Add local metadata models 2025-03-26 22:08:43 +05:30
Neeraj Gupta
f7330be52c Reduce queue size 2025-03-26 16:44:08 +05:30
Neeraj Gupta
4cce54a0c6 Use local files for search 2025-03-26 16:42:15 +05:30
Neeraj Gupta
1850e9a2a6 Refactor 2025-03-26 16:24:51 +05:30
Neeraj Gupta
21e2b589cc Minor refactor 2025-03-26 16:23:34 +05:30
Neeraj Gupta
b7f8deb452 Disable smart memories in debug mode 2025-03-26 16:22:59 +05:30
Neeraj Gupta
b6ffb3ca22 Simplify schema 2025-03-26 14:39:29 +05:30
Neeraj Gupta
9351a52800 Clean up imports 2025-03-26 12:46:03 +05:30
Neeraj Gupta
c7510024c0 Use db query to get files on on device albums 2025-03-26 12:43:53 +05:30
Neeraj Gupta
63d3b1c94b Show files within on device albums 2025-03-25 16:10:50 +05:30
Neeraj Gupta
cec27b40a4 Show local device albums 2025-03-25 15:41:16 +05:30
Neeraj Gupta
2f8d0d1957 Delete old local sync 2025-03-25 15:10:49 +05:30
Neeraj Gupta
7f0a36f110 Merge branch 'main' into remote_db 2025-03-25 14:29:12 +05:30
Neeraj Gupta
355367a601 Stop photoManager callback for old local sync 2025-03-25 13:19:09 +05:30
Neeraj Gupta
c66da422cd Update local asset cache 2025-03-25 12:31:15 +05:30
Neeraj Gupta
6f90fad4a2 Fix: Store localSync time in ms 2025-03-25 12:30:52 +05:30
Neeraj Gupta
bcd6f55376 Fix: Use parameterized query params 2025-03-25 12:27:26 +05:30
Neeraj Gupta
b2766a0d4f Use thumbnail widget in the backfolder screen 2025-03-25 11:38:35 +05:30
Neeraj Gupta
ec1b95b0cd Merge branch 'main' into remote_db 2025-03-25 11:35:03 +05:30
Neeraj Gupta
4369317a4d Use lock for full sync 2025-03-25 05:47:47 +05:30
Neeraj Gupta
b18298dc62 Merge remote-tracking branch 'origin/main' into remote_db 2025-03-25 05:43:36 +05:30
Neeraj Gupta
b3e467a1a4 Return early 2025-03-25 05:43:21 +05:30
Neeraj Gupta
f8cd3d9fb4 Add retry on task cancelation 2025-03-24 17:15:18 +05:30
Neeraj Gupta
6d4756ca4b Undo cancellation 2025-03-24 16:16:44 +05:30
Neeraj Gupta
676bbb4d88 Add thumb queue 2025-03-24 16:14:43 +05:30
Neeraj Gupta
da8edfd34e Use new cache for inMem thumbnails 2025-03-24 15:43:42 +05:30
Neeraj Gupta
bf453cfaac Fix selection 2025-03-24 15:10:22 +05:30
Neeraj Gupta
35f41f044e Load thumbnail from cache 2025-03-24 15:09:36 +05:30
Neeraj Gupta
acff269695 Add global cache for image thumbnail 2025-03-24 14:58:24 +05:30
Neeraj Gupta
2b4f96dbb7 Remove task queue from existing thumb fetch 2025-03-24 14:01:50 +05:30
Neeraj Gupta
d5796e2abb Use local thumb provider for thumbnails 2025-03-24 14:00:59 +05:30
Neeraj Gupta
afe9690891 Keep reference for assetEntity 2025-03-24 11:39:20 +05:30
Neeraj Gupta
fc619bbd03 Use task queue to throttle local thumbnail fetch 2025-03-22 21:53:31 +05:30
Neeraj Gupta
a198331ffd Skip reading on disk cache for local thumbnails 2025-03-22 16:26:35 +05:30
Neeraj Gupta
c58fe5358d Add task queue 2025-03-22 16:13:17 +05:30
Neeraj Gupta
defc5164b9 Merge remote-tracking branch 'origin/main' into remote_db 2025-03-22 15:17:42 +05:30
Neeraj Gupta
97d4fb0693 Generated changes 2025-03-22 15:17:30 +05:30
Neeraj Gupta
e708564cb9 Show local assets on home screen 2025-03-22 15:13:19 +05:30
Neeraj Gupta
92068e026b Fix mapping 2025-03-22 14:16:07 +05:30
Neeraj Gupta
8a079ab4f4 Disable existing local & remote sync 2025-03-22 13:27:44 +05:30
Neeraj Gupta
b41c57cb8d Speed up backup folder selection page 2025-03-22 13:26:20 +05:30
Neeraj Gupta
87167e49fc Remove deadlock 2025-03-21 16:59:20 +05:30
Neeraj Gupta
089d1dcd10 Copy ios proj from main 2025-03-21 14:26:03 +05:30
Neeraj Gupta
db02e66124 Merge branch 'main' into remote_db 2025-03-21 14:18:05 +05:30
Neeraj Gupta
964066bf31 Merge remote-tracking branch 'origin/main' into remote_db 2025-03-21 11:09:22 +05:30
Neeraj Gupta
643220e595 Merge branch 'main' into remote_db 2025-03-20 16:47:55 +05:30
Neeraj Gupta
e871498161 Disable remote sync 2025-03-20 15:06:06 +05:30
Neeraj Gupta
31a88b74df Merge branch 'main' into remote_db 2025-03-20 14:05:00 +05:30
Neeraj Gupta
1801258fea Show thumbnail on backup folder 2025-03-19 22:33:22 +05:30
Neeraj Gupta
27acc2125b Fix log 2025-03-19 21:45:48 +05:30
Neeraj Gupta
60b7a91756 Add support for caching local db 2025-03-19 21:45:31 +05:30
Neeraj Gupta
b38a01820d Avoid redundant db txn 2025-03-19 16:25:18 +05:30
Neeraj Gupta
347d5a7a72 Add local image provider for thumbnail 2025-03-19 16:03:47 +05:30
Neeraj Gupta
82979ac729 Fix bug in computing fullDiff 2025-03-19 14:09:22 +05:30
Neeraj Gupta
5768eeb1fe Use trigger instead of fk to avoid redundant deletion 2025-03-19 14:07:58 +05:30
Neeraj Gupta
7a9ff9877a Add new screen for backup 2025-03-19 14:00:51 +05:30
Neeraj Gupta
3565540a61 get assetPath from local 2025-03-19 11:34:41 +05:30
Neeraj Gupta
06d78e5d6a Merge remote-tracking branch 'origin/main' into remote_db 2025-03-19 10:29:54 +05:30
Neeraj Gupta
6c11b76c11 [mob] Refactor 2025-03-17 17:16:43 +05:30
Neeraj Gupta
749cfde7d8 [mob] Support for enabling foreign key 2025-03-17 16:28:49 +05:30
Neeraj Gupta
6bcaa8ae26 [mob] Refactor 2025-03-17 16:27:12 +05:30
Neeraj Gupta
99c6318b0e Merge branch 'db_refactor' into remote_db 2025-03-17 15:35:02 +05:30
Neeraj Gupta
abf789a4aa rename 2025-03-17 14:12:39 +05:30
Neeraj Gupta
f5d3712cbb [mob] Remove unused code + update debouncer 2025-03-17 13:58:01 +05:30
Neeraj Gupta
45dd540abc [mob] move 2025-03-17 13:40:29 +05:30
Neeraj Gupta
865a736bdd [mob] Switch to leading debouncer 2025-03-17 13:29:35 +05:30
Neeraj Gupta
75bd1bfef6 [mob] Avoid running two fullSync 2025-03-17 13:27:57 +05:30
Neeraj Gupta
4cf67fe171 [mob] Fix delete from local db 2025-03-17 12:32:11 +05:30
Neeraj Gupta
65895328dc [mob] Fix full diff computation 2025-03-17 11:46:03 +05:30
Neeraj Gupta
a1c20b9c8a Merge remote-tracking branch 'origin/main' into remote_db 2025-03-17 11:25:45 +05:30
Neeraj Gupta
de9def5370 [mob] Rename 2025-03-17 11:25:24 +05:30
Neeraj Gupta
9cc723a280 [mob] Local local device assets info in db 2025-03-17 11:03:04 +05:30
Neeraj Gupta
16beae2a82 [mob] Rename 2025-03-15 21:56:10 +05:30
Neeraj Gupta
2e25c38324 [mob] Store local assets state in db 2025-03-15 21:53:47 +05:30
Neeraj Gupta
7e691f84e4 Merge remote-tracking branch 'origin/main' into remote_db 2025-03-15 16:24:42 +05:30
Neeraj Gupta
d99593fc85 [mob] Fix impl diff compute & simplify 2025-03-14 18:30:11 +05:30
Neeraj Gupta
d4877ea446 [mob] Write in parallel 2025-03-14 17:05:16 +05:30
Neeraj Gupta
5f4c748886 [mob] Refactor & improve logs 2025-03-14 16:17:59 +05:30
Neeraj Gupta
eaab58c62a [mob] refactor 2025-03-14 13:48:34 +05:30
Neeraj Gupta
c1c020402e [mob] Simplify local import 2025-03-14 13:10:44 +05:30
Neeraj Gupta
028f4e61d2 [mob] Add initial db schema for local files 2025-03-13 17:19:47 +05:30
Neeraj Gupta
abc6f56247 [mob] Fix: Handle null nonce for shared collections 2025-03-13 17:12:26 +05:30
Neeraj Gupta
822eb59761 Merge branch 'main' into remote_db 2025-03-13 15:07:20 +05:30
Neeraj Gupta
fade7859ab Refactor 2025-03-13 11:34:33 +05:30
Neeraj Gupta
f85047fb28 [mob] Refactor 2025-03-13 11:21:50 +05:30
Neeraj Gupta
3f81c9beae [mob] Add helpers for local assets import 2025-03-13 09:55:53 +05:30
Neeraj Gupta
b7fa8d7c89 Merge branch 'main' into remote_db 2025-03-12 13:59:12 +05:30
Neeraj Gupta
5f75c5fc3f [mob] Add sql scripts for local db 2025-03-11 14:06:13 +05:30
Neeraj Gupta
98eaee3b9e Merge branch 'main' into remote_db 2025-03-11 13:35:36 +05:30
Neeraj Gupta
59bd039bed Add db schema for local files 2025-03-10 11:07:17 +05:30
Neeraj Gupta
3e90126a55 Merge branch 'main' into remote_db 2025-03-07 13:47:42 +05:30
Neeraj Gupta
dabcc0aeb5 Merge remote-tracking branch 'origin/main' into remote_db 2025-03-05 13:35:37 +05:30
Neeraj Gupta
c4b99af0e2 Merge branch 'main' into remote_db 2025-03-03 19:07:38 +05:30
Neeraj Gupta
679c12bb90 Remove recursion 2025-03-03 11:21:11 +05:30
Neeraj Gupta
3ef8ece8c0 Refactor 2025-03-03 11:16:43 +05:30
Neeraj Gupta
24f8cf188a Refactor 2025-03-03 10:34:38 +05:30
Neeraj Gupta
233838da3e Merge branch 'main' into remote_db 2025-03-01 12:37:42 +05:30
Neeraj Gupta
06c4866c75 [mob] Refactor 2025-02-28 14:07:01 +05:30
Neeraj Gupta
fa71acf91a Merge branch 'main' into remote_db 2025-02-28 13:51:53 +05:30
Neeraj Gupta
81d40826b3 Merge remote-tracking branch 'origin/main' into remote_db 2025-02-28 12:59:40 +05:30
Neeraj Gupta
2a14b5e5a3 clean up 2025-02-28 12:59:26 +05:30
Neeraj Gupta
63bbca09f3 [mob] clean up 2025-02-28 11:39:20 +05:30
Neeraj Gupta
cbff68bc42 [server] Persist remote pull response 2025-02-28 10:56:15 +05:30
Neeraj Gupta
070ab80be9 [mob] Enable foreign_keys on remote db 2025-02-27 14:12:26 +05:30
Neeraj Gupta
0efbf407d3 [mob] table for collection_files and files 2025-02-27 14:11:52 +05:30
Neeraj Gupta
ab22b28695 Merge branch 'main' into remote_db 2025-02-26 16:13:26 +05:30
Neeraj Gupta
d3466d7efe [mob] Insert collection_files 2025-02-25 05:30:46 +05:30
Neeraj Gupta
5945f2aaad Merge branch 'main' into remote_db 2025-02-24 15:11:30 +05:30
Neeraj Gupta
4c79b9cb92 [mob] Pull remote diff using new diff models 2025-02-20 11:57:36 +05:30
Neeraj Gupta
4676c363d2 [mob] Refactor 2025-02-20 11:55:46 +05:30
Neeraj Gupta
f75807d8f0 [mob] New diff model 2025-02-20 11:24:02 +05:30
Neeraj Gupta
578541308a [mob] Refactor 2025-02-19 15:51:41 +05:30
Neeraj Gupta
30cded4d3d [mob][crypto] Sync stream decryption 2025-02-19 15:51:13 +05:30
Neeraj Gupta
be2aee6baa Merge branch 'main' into remote_db 2025-02-18 15:21:25 +05:30
Neeraj Gupta
fae23df6eb [mob] refactor 2025-02-17 14:37:09 +05:30
Neeraj Gupta
2a1f2aded1 [mob] Use remote db for collections 2025-02-14 21:23:01 +05:30
Neeraj Gupta
6e85d24286 [mob] Add db row mappers for collection 2025-02-14 21:23:01 +05:30
Neeraj Gupta
0bcc676e44 [mob] Add new collection model for remote 2025-02-14 21:23:01 +05:30
Neeraj Gupta
b5a9bab5c6 refactor 2025-02-14 21:23:01 +05:30
Neeraj Gupta
5849d14cd9 refactor 2025-02-14 21:23:01 +05:30
Neeraj Gupta
77cde87927 [mob] Create db for collections & collection_files 2025-02-14 21:23:01 +05:30
Neeraj Gupta
670d6e8470 [mob] Scaffold remote db 2025-02-14 21:23:01 +05:30
248 changed files with 12618 additions and 7859 deletions

View File

@@ -1,3 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
=description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -181,6 +181,8 @@ PODS:
- PromisesObjC (2.4.0)
- receive_sharing_intent (1.8.1):
- Flutter
- rive_common (0.0.1):
- Flutter
- rust_lib_photos (0.0.1):
- Flutter
- SDWebImage (5.21.1):
@@ -291,6 +293,7 @@ DEPENDENCIES:
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- privacy_screen (from `.symlinks/plugins/privacy_screen/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- rive_common (from `.symlinks/plugins/rive_common/ios`)
- rust_lib_photos (from `.symlinks/plugins/rust_lib_photos/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
@@ -309,7 +312,7 @@ DEPENDENCIES:
- workmanager (from `.symlinks/plugins/workmanager/ios`)
SPEC REPOS:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios:
https://github.com/ente-io/ffmpeg-kit-custom-repo-ios.git:
- ffmpeg_kit_custom
trunk:
- Firebase
@@ -418,6 +421,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/privacy_screen/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
rive_common:
:path: ".symlinks/plugins/rive_common/ios"
rust_lib_photos:
:path: ".symlinks/plugins/rust_lib_photos/ios"
sentry_flutter:
@@ -452,84 +457,85 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
app_links: 76b66b60cc809390ca1ad69bfd66b998d2387ac7
battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
emoji_picker_flutter: ed468d9746c21711e66b2788880519a9de5de211
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
battery_info: b6c551049266af31556b93c9d9b9452cfec0219f
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
emoji_picker_flutter: fe2e6151c5b548e975d546e6eeb567daf0962a58
ffmpeg_kit_custom: 682b4f2f1ff1f8abae5a92f6c3540f2441d5be99
ffmpeg_kit_flutter: 915b345acc97d4142e8a9a8549d177ff10f043f5
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
ffmpeg_kit_flutter: 9dce4803991478c78c6fb9f972703301101095fe
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: ece862f94b2bc72ee0edbeec7ab5c7cb09fe1ab5
firebase_messaging: e1a5fae495603115be1d0183bc849da748734e2b
firebase_core: cf4d42a8ac915e51c0c2dc103442f3036d941a2d
firebase_messaging: fee490327c1aae28a0da1e65fca856547deca493
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58
flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_sodium: a00383520fc689c688b66fd3092984174712493e
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
launcher_icon_switcher: 84c218d233505aa7d8655d8fa61a3ba802c022da
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
launcher_icon_switcher: 8e0ad2131a20c51c1dd939896ee32e70cd845b37
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451
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: 6809dec117e8997161dbfb42a6f90d6df71a504d
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2
native_video_player: 29ab24a926804ac8c4a57eb6d744c7d927c2bc3e
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: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: 81954a1bf804b6e882d0453b3b6bc7fad7b47d3d
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
rust_lib_photos: 04d3901908d2972192944083310b65abf410774c
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
rive_common: 4743dbfd2911c99066547a3c6454681e0fa907df
rust_lib_photos: 8813b31af48ff02ca75520cbc81a363a13d51a84
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 1d85290c3321153511f6e900ede7a1608718bbd5
sqlite3_flutter_libs: e7fc8c9ea2200ff3271f08f127842131746b70e2
system_info_plus: 555ce7047fbbf29154726db942ae785c29211740
thermal: d4c48be750d1ddbab36b0e2dcb2471531bc8df41
ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
workmanager: b89e4e4445d8b57ee2fdbf1c3925696ebe5b8990
sqlite3_flutter_libs: 2c48c4ee7217fd653251975e43412250d5bcbbe2
system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa
thermal: a9261044101ae8f532fa29cab4e8270b51b3f55c
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1
volume_controller: 2e3de73d6e7e81a0067310d17fb70f2f86d71ac7
wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e
workmanager: 05afacf221f5086e18450250dce57f59bb23e6b0
PODFILE CHECKSUM: cce2cd3351d3488dca65b151118552b680e23635

View File

@@ -565,6 +565,7 @@
"${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework",
"${BUILT_PRODUCTS_DIR}/privacy_screen/privacy_screen.framework",
"${BUILT_PRODUCTS_DIR}/receive_sharing_intent/receive_sharing_intent.framework",
"${BUILT_PRODUCTS_DIR}/rive_common/rive_common.framework",
"${BUILT_PRODUCTS_DIR}/rust_lib_photos/rust_lib_photos.framework",
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
@@ -662,6 +663,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/privacy_screen.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/receive_sharing_intent.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/rive_common.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/rust_lib_photos.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",

View File

@@ -29,6 +29,10 @@ class LRUMap<K, V> {
}
}
bool containsKey(K key) {
return _map.containsKey(key);
}
void remove(K key) {
_map.remove(key);
}

View File

@@ -1,39 +0,0 @@
import 'dart:typed_data';
import 'package:photos/core/cache/lru_map.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/models/file/file.dart';
class ThumbnailInMemoryLruCache {
static final LRUMap<String, Uint8List?> _map = LRUMap(1000);
static Uint8List? get(EnteFile enteFile, [int? size]) {
return _map.get(
enteFile.cacheKey() +
"_" +
(size != null ? size.toString() : thumbnailLargeSize.toString()),
);
}
static void put(
EnteFile enteFile,
Uint8List? imageData, [
int? size,
]) {
_map.put(
enteFile.cacheKey() +
"_" +
(size != null ? size.toString() : thumbnailLargeSize.toString()),
imageData,
);
}
static void clearCache(EnteFile enteFile) {
_map.remove(
enteFile.cacheKey() + "_" + thumbnailLargeSize.toString(),
);
_map.remove(
enteFile.cacheKey() + "_" + thumbnailSmallSize.toString(),
);
}
}

View File

@@ -11,11 +11,9 @@ import 'package:path_provider/path_provider.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/error-reporting/super_logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/collections_db.dart';
import 'package:photos/db/files_db.dart';
import "package:photos/db/memories_db.dart";
import "package:photos/db/ml/db.dart";
import 'package:photos/db/trash_db.dart';
import 'package:photos/db/upload_locks_db.dart';
import "package:photos/events/endpoint_updated_event.dart";
import 'package:photos/events/signed_in_event.dart';
@@ -23,6 +21,8 @@ import 'package:photos/events/user_logged_out_event.dart';
import 'package:photos/models/api/user/key_attributes.dart';
import 'package:photos/models/api/user/key_gen_result.dart';
import 'package:photos/models/api/user/private_key_attributes.dart';
import 'package:photos/module/upload/service/file_uploader.dart';
import "package:photos/service_locator.dart";
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/favorites_service.dart';
import "package:photos/services/home_widget_service.dart";
@@ -31,7 +31,6 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d
import "package:photos/services/machine_learning/similar_images_service.dart";
import 'package:photos/services/search_service.dart';
import 'package:photos/services/sync/sync_service.dart';
import 'package:photos/utils/file_uploader.dart';
import "package:photos/utils/lock_screen_settings.dart";
import 'package:photos/utils/validator_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -194,14 +193,13 @@ class Configuration {
_cachedToken = null;
_secretKey = null;
await FilesDB.instance.clearTable();
await CollectionsDB.instance.clearTable();
// await CollectionsDB.instance.clearTable();
await MemoriesDB.instance.clearTable();
await MLDataDB.instance.clearTable();
await remoteDB.clearAllTables();
await SimilarImagesService.instance.clearCache();
await UploadLocksDB.instance.clearTable();
await IgnoredFilesService.instance.reset();
await TrashDB.instance.clearTable();
unawaited(HomeWidgetService.instance.clearWidget(autoLogout));
if (!autoLogout) {
// Following services won't be initialized if it's the case of autoLogout

View File

@@ -1,10 +1,12 @@
import "package:flutter/foundation.dart";
const int thumbnailSmallSize = 256;
const int thumbnailQuality = 50;
const int thumbnailLargeSize = 512;
const int compressedThumbnailResolution = 1080;
const int thumbnailDataLimit = 100 * 1024;
// thumbnailSmallSize Thumbnail sizes in pixels 256px
const int thumbnailSmall256 = 256;
// thumbnailMediumSize Thumbnail sizes in pixels 512px
const int thumbnailLarge512 = 512; // 512px
const int compressThumb1080 = 1080;
const int thumbnailDataMaxSize = 100 * 1024;
const String sentryDSN =
"https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4";
const String sentryDebugDSN =
@@ -109,4 +111,4 @@ final tempDirCleanUpInterval = kDebugMode
const kFilterChipHeight = 32.0;
const kMaxAppbarFilters = 14;
const kLivePhotoHashSeparator = ':';
const kHashSeprator = ':';

View File

@@ -1,322 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import "package:photos/models/api/collection/public_url.dart";
import "package:photos/models/api/collection/user.dart";
import 'package:photos/models/collection/collection.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_migration/sqflite_migration.dart';
class CollectionsDB {
static const _databaseName = "ente.collections.db";
static const table = 'collections';
static const tempTable = 'temp_collections';
static const _sqlBoolTrue = 1;
static const _sqlBoolFalse = 0;
static const columnID = 'collection_id';
static const columnOwner = 'owner';
static const columnEncryptedKey = 'encrypted_key';
static const columnKeyDecryptionNonce = 'key_decryption_nonce';
static const columnName = 'name';
static const columnEncryptedName = 'encrypted_name';
static const columnNameDecryptionNonce = 'name_decryption_nonce';
static const columnType = 'type';
static const columnEncryptedPath = 'encrypted_path';
static const columnPathDecryptionNonce = 'path_decryption_nonce';
static const columnVersion = 'version';
static const columnSharees = 'sharees';
static const columnPublicURLs = 'public_urls';
// MMD -> Magic Metadata
static const columnMMdEncodedJson = 'mmd_encoded_json';
static const columnMMdVersion = 'mmd_ver';
static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json';
static const columnPubMMdVersion = 'pub_mmd_ver';
static const columnSharedMMdJson = 'shared_mmd_json';
static const columnSharedMMdVersion = 'shared_mmd_ver';
static const columnUpdationTime = 'updation_time';
static const columnIsDeleted = 'is_deleted';
static final intitialScript = [...createTable(table)];
static final migrationScripts = [
...alterNameToAllowNULL(),
...addEncryptedName(),
...addVersion(),
...addIsDeleted(),
...addPublicURLs(),
...addPrivateMetadata(),
...addPublicMetadata(),
...addShareeMetadata(),
];
final dbConfig = MigrationConfig(
initializationScript: intitialScript,
migrationScripts: migrationScripts,
);
CollectionsDB._privateConstructor();
static final CollectionsDB instance = CollectionsDB._privateConstructor();
static Future<Database>? _dbFuture;
Future<Database> get database async {
_dbFuture ??= _initDatabase();
return _dbFuture!;
}
Future<Database> _initDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
return await openDatabaseWithMigration(path, dbConfig);
}
Future<void> clearTable() async {
final db = await instance.database;
await db.delete(table);
}
static List<String> createTable(String tableName) {
return [
'''
CREATE TABLE $tableName (
$columnID INTEGER PRIMARY KEY NOT NULL,
$columnOwner TEXT NOT NULL,
$columnEncryptedKey TEXT NOT NULL,
$columnKeyDecryptionNonce TEXT,
$columnName TEXT,
$columnType TEXT NOT NULL,
$columnEncryptedPath TEXT,
$columnPathDecryptionNonce TEXT,
$columnSharees TEXT,
$columnUpdationTime TEXT NOT NULL
);
'''
];
}
static List<String> alterNameToAllowNULL() {
return [
...createTable(tempTable),
'''
INSERT INTO $tempTable
SELECT *
FROM $table;
DROP TABLE $table;
ALTER TABLE $tempTable
RENAME TO $table;
'''
];
}
static List<String> addEncryptedName() {
return [
'''
ALTER TABLE $table
ADD COLUMN $columnEncryptedName TEXT;
''',
'''ALTER TABLE $table
ADD COLUMN $columnNameDecryptionNonce TEXT;
'''
];
}
static List<String> addVersion() {
return [
'''
ALTER TABLE $table
ADD COLUMN $columnVersion INTEGER DEFAULT 0;
'''
];
}
static List<String> addIsDeleted() {
return [
'''
ALTER TABLE $table
ADD COLUMN $columnIsDeleted INTEGER DEFAULT $_sqlBoolFalse;
'''
];
}
static List<String> addPublicURLs() {
return [
'''
ALTER TABLE $table
ADD COLUMN $columnPublicURLs TEXT;
'''
];
}
static List<String> addPrivateMetadata() {
return [
'''
ALTER TABLE $table ADD COLUMN $columnMMdEncodedJson TEXT DEFAULT '{}';
''',
'''
ALTER TABLE $table ADD COLUMN $columnMMdVersion INTEGER DEFAULT 0;
'''
];
}
static List<String> addPublicMetadata() {
return [
'''
ALTER TABLE $table ADD COLUMN $columnPubMMdEncodedJson TEXT DEFAULT '
{}';
''',
'''
ALTER TABLE $table ADD COLUMN $columnPubMMdVersion INTEGER DEFAULT 0;
'''
];
}
static List<String> addShareeMetadata() {
return [
'''
ALTER TABLE $table ADD COLUMN $columnSharedMMdJson TEXT DEFAULT '
{}';
''',
'''
ALTER TABLE $table ADD COLUMN $columnSharedMMdVersion INTEGER DEFAULT 0;
'''
];
}
Future<void> insert(List<Collection> collections) async {
final db = await instance.database;
var batch = db.batch();
int batchCounter = 0;
for (final collection in collections) {
if (batchCounter == 400) {
await batch.commit(noResult: true);
batch = db.batch();
batchCounter = 0;
}
batch.insert(
table,
_getRowForCollection(collection),
conflictAlgorithm: ConflictAlgorithm.replace,
);
batchCounter++;
}
await batch.commit(noResult: true);
}
Future<List<Collection>> getAllCollections() async {
final db = await instance.database;
final rows = await db.query(table);
final collections = <Collection>[];
for (final row in rows) {
collections.add(_convertToCollection(row));
}
return collections;
}
// getActiveCollectionIDsAndUpdationTime returns map of collectionID to
// updationTime for non-deleted collections
Future<Map<int, int>> getActiveIDsAndRemoteUpdateTime() async {
final db = await instance.database;
final rows = await db.query(
table,
where: '($columnIsDeleted = ? OR $columnIsDeleted IS NULL)',
whereArgs: [_sqlBoolFalse],
columns: [columnID, columnUpdationTime],
);
final collectionIDsAndUpdationTime = <int, int>{};
for (final row in rows) {
collectionIDsAndUpdationTime[row[columnID] as int] =
int.parse(row[columnUpdationTime] as String);
}
return collectionIDsAndUpdationTime;
}
Future<int> deleteCollection(int collectionID) async {
final db = await instance.database;
return db.delete(
table,
where: '$columnID = ?',
whereArgs: [collectionID],
);
}
Map<String, dynamic> _getRowForCollection(Collection collection) {
final row = <String, dynamic>{};
row[columnID] = collection.id;
row[columnOwner] = collection.owner.toJson();
row[columnEncryptedKey] = collection.encryptedKey;
row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
// ignore: deprecated_member_use_from_same_package
row[columnName] = collection.name;
row[columnEncryptedName] = collection.encryptedName;
row[columnNameDecryptionNonce] = collection.nameDecryptionNonce;
row[columnType] = typeToString(collection.type);
row[columnEncryptedPath] = collection.attributes.encryptedPath;
row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce;
row[columnVersion] = collection.attributes.version;
row[columnSharees] =
json.encode(collection.sharees.map((x) => x.toMap()).toList());
row[columnPublicURLs] =
json.encode(collection.publicURLs.map((x) => x.toMap()).toList());
row[columnUpdationTime] = collection.updationTime;
if (collection.isDeleted) {
row[columnIsDeleted] = _sqlBoolTrue;
} else {
row[columnIsDeleted] = _sqlBoolFalse;
}
row[columnMMdVersion] = collection.mMdVersion;
row[columnMMdEncodedJson] = collection.mMdEncodedJson ?? '{}';
row[columnPubMMdVersion] = collection.mMbPubVersion;
row[columnPubMMdEncodedJson] = collection.mMdPubEncodedJson ?? '{}';
row[columnSharedMMdVersion] = collection.sharedMmdVersion;
row[columnSharedMMdJson] = collection.sharedMmdJson ?? '{}';
return row;
}
Collection _convertToCollection(Map<String, dynamic> row) {
final Collection result = Collection(
row[columnID],
User.fromJson(row[columnOwner]),
row[columnEncryptedKey],
row[columnKeyDecryptionNonce],
row[columnName],
row[columnEncryptedName],
row[columnNameDecryptionNonce],
typeFromString(row[columnType]),
CollectionAttributes(
encryptedPath: row[columnEncryptedPath],
pathDecryptionNonce: row[columnPathDecryptionNonce],
version: row[columnVersion],
),
List<User>.from(
(json.decode(row[columnSharees]) as List).map((x) => User.fromMap(x)),
),
row[columnPublicURLs] == null
? []
: List<PublicURL>.from(
(json.decode(row[columnPublicURLs]) as List)
.map((x) => PublicURL.fromMap(x)),
),
int.parse(row[columnUpdationTime]),
// default to False is columnIsDeleted is not set
isDeleted: (row[columnIsDeleted] ?? _sqlBoolFalse) == _sqlBoolTrue,
);
result.mMdVersion = row[columnMMdVersion] ?? 0;
result.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
result.mMbPubVersion = row[columnPubMMdVersion] ?? 0;
result.mMdPubEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
result.sharedMmdVersion = row[columnSharedMMdVersion] ?? 0;
result.sharedMmdJson = row[columnSharedMMdJson] ?? '{}';
return result;
}
}

View File

@@ -2,9 +2,9 @@ import "package:flutter/foundation.dart";
import "package:sqlite_async/sqlite_async.dart";
mixin SqlDbBase {
static const _params = {};
static final _params = {};
static String getParams(int count) {
String getParams(int count) {
if (!_params.containsKey(count)) {
final params = List.generate(count, (_) => "?").join(", ");
_params[count] = params;
@@ -14,9 +14,13 @@ mixin SqlDbBase {
Future<void> migrate(
SqliteDatabase database,
List<String> migrationScripts,
) async {
List<String> migrationScripts, {
bool onForeignKey = false,
}) async {
final result = await database.execute('PRAGMA user_version');
if (onForeignKey) {
await database.execute("PRAGMA foreign_keys = ON");
}
final currentVersion = result[0]['user_version'] as int;
final toVersion = migrationScripts.length;

View File

@@ -1,492 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/backup_status.dart';
import 'package:photos/models/device_collection.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/upload_strategy.dart';
import "package:photos/services/sync/import/model.dart";
import 'package:sqflite/sqlite_api.dart';
import 'package:tuple/tuple.dart';
extension DeviceFiles on FilesDB {
static final Logger _logger = Logger("DeviceFilesDB");
static const _sqlBoolTrue = 1;
static const _sqlBoolFalse = 0;
Future<void> insertPathIDToLocalIDMapping(
Map<String, Set<String>> mappingToAdd, {
ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore,
}) async {
debugPrint("Inserting missing PathIDToLocalIDMapping");
final parameterSets = <List<Object?>>[];
int batchCounter = 0;
for (MapEntry e in mappingToAdd.entries) {
final String pathID = e.key;
for (String localID in e.value) {
parameterSets.add([localID, pathID]);
batchCounter++;
if (batchCounter == 400) {
await _insertBatch(parameterSets, conflictAlgorithm);
parameterSets.clear();
batchCounter = 0;
}
}
}
await _insertBatch(parameterSets, conflictAlgorithm);
parameterSets.clear();
batchCounter = 0;
}
Future<void> deletePathIDToLocalIDMapping(
Map<String, Set<String>> mappingsToRemove,
) async {
debugPrint("removing PathIDToLocalIDMapping");
final parameterSets = <List<Object?>>[];
int batchCounter = 0;
for (MapEntry e in mappingsToRemove.entries) {
final String pathID = e.key;
for (String localID in e.value) {
parameterSets.add([localID, pathID]);
batchCounter++;
if (batchCounter == 400) {
await _deleteBatch(parameterSets);
parameterSets.clear();
batchCounter = 0;
}
}
}
await _deleteBatch(parameterSets);
parameterSets.clear();
batchCounter = 0;
}
Future<Map<String, int>> getDevicePathIDToImportedFileCount() async {
try {
final db = await sqliteAsyncDB;
final rows = await db.getAll(
'''
SELECT count(*) as count, path_id
FROM device_files
GROUP BY path_id
''',
);
final result = <String, int>{};
for (final row in rows) {
result[row['path_id'] as String] = row["count"] as int;
}
return result;
} catch (e) {
_logger.severe("failed to getDevicePathIDToImportedFileCount", e);
rethrow;
}
}
Future<Map<String, Set<String>>> getDevicePathIDToLocalIDMap() async {
try {
final db = await sqliteAsyncDB;
final rows = await db.getAll(
''' SELECT id, path_id FROM device_files; ''',
);
final result = <String, Set<String>>{};
for (final row in rows) {
final String pathID = row['path_id'] as String;
if (!result.containsKey(pathID)) {
result[pathID] = <String>{};
}
result[pathID]!.add(row['id'] as String);
}
return result;
} catch (e) {
_logger.severe("failed to getDevicePathIDToLocalIDMap", e);
rethrow;
}
}
Future<Set<String>> getDevicePathIDs() async {
final db = await sqliteAsyncDB;
final rows = await db.getAll(
'''
SELECT id FROM device_collections
''',
);
final Set<String> result = <String>{};
for (final row in rows) {
result.add(row['id'] as String);
}
return result;
}
Future<void> insertLocalAssets(
List<LocalPathAsset> localPathAssets, {
bool shouldAutoBackup = false,
}) async {
final db = await sqliteAsyncDB;
final Map<String, Set<String>> pathIDToLocalIDsMap = {};
try {
final Set<String> existingPathIds = await getDevicePathIDs();
final parameterSetsForUpdate = <List<Object?>>[];
final parameterSetsForInsert = <List<Object?>>[];
for (LocalPathAsset localPathAsset in localPathAssets) {
if (localPathAsset.localIDs.isNotEmpty) {
pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs;
}
if (existingPathIds.contains(localPathAsset.pathID)) {
parameterSetsForUpdate
.add([localPathAsset.pathName, localPathAsset.pathID]);
} else if (localPathAsset.localIDs.isNotEmpty) {
parameterSetsForInsert.add([
localPathAsset.pathID,
localPathAsset.pathName,
shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse,
]);
}
}
await db.executeBatch(
'''
INSERT OR IGNORE INTO device_collections (id, name, should_backup) VALUES (?, ?, ?);
''',
parameterSetsForInsert,
);
await db.executeBatch(
'''
UPDATE device_collections SET name = ? WHERE id = ?;
''',
parameterSetsForUpdate,
);
// add the mappings for localIDs
if (pathIDToLocalIDsMap.isNotEmpty) {
await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap);
}
} catch (e) {
_logger.severe("failed to save path names", e);
rethrow;
}
}
Future<bool> updateDeviceCoverWithCount(
List<Tuple2<AssetPathEntity, String>> devicePathInfo, {
bool shouldBackup = false,
}) async {
bool hasUpdated = false;
try {
final db = await sqliteAsyncDB;
final Set<String> existingPathIds = await getDevicePathIDs();
for (Tuple2<AssetPathEntity, String> tup in devicePathInfo) {
final AssetPathEntity pathEntity = tup.item1;
final assetCount = await pathEntity.assetCountAsync;
final String localID = tup.item2;
final bool shouldUpdate = existingPathIds.contains(pathEntity.id);
if (shouldUpdate) {
final rowUpdated = await db.writeTransaction((tx) async {
await tx.execute(
"UPDATE device_collections SET name = ?, cover_id = ?, count"
" = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)",
[
pathEntity.name,
localID,
assetCount,
pathEntity.id,
pathEntity.name,
localID,
assetCount,
],
);
final result = await tx.get("SELECT changes();");
return result["changes()"] as int;
});
if (rowUpdated > 0) {
_logger.info("Updated $rowUpdated rows for ${pathEntity.name}");
hasUpdated = true;
}
} else {
hasUpdated = true;
await db.execute(
'''
INSERT INTO device_collections (id, name, count, cover_id, should_backup)
VALUES (?, ?, ?, ?, ?);
''',
[
pathEntity.id,
pathEntity.name,
assetCount,
localID,
shouldBackup ? _sqlBoolTrue : _sqlBoolFalse,
],
);
}
}
// delete existing pathIDs which are missing on device
existingPathIds.removeAll(devicePathInfo.map((e) => e.item1.id).toSet());
if (existingPathIds.isNotEmpty) {
hasUpdated = true;
_logger.info(
'Deleting non-backed up pathIds from local '
'$existingPathIds',
);
for (String pathID in existingPathIds) {
// do not delete device collection entries for paths which are
// marked for backup. This is to handle "Free up space"
// feature, where we delete files which are backed up. Deleting such
// entries here result in us losing out on the information that
// those folders were marked for automatic backup.
await db.execute(
'''
DELETE FROM device_collections WHERE id = ? AND should_backup = $_sqlBoolFalse;
''',
[pathID],
);
await db.execute(
'''
DELETE FROM device_files WHERE path_id = ?;
''',
[pathID],
);
}
}
return hasUpdated;
} catch (e) {
_logger.severe("failed to save path names", e);
rethrow;
}
}
// getDeviceSyncCollectionIDs returns the collectionIDs for the
// deviceCollections which are marked for auto-backup
Future<Set<int>> getDeviceSyncCollectionIDs() async {
final db = await sqliteAsyncDB;
final rows = await db.getAll(
'''
SELECT collection_id FROM device_collections where should_backup =
$_sqlBoolTrue
and collection_id != -1;
''',
);
final Set<int> result = <int>{};
for (final row in rows) {
result.add(row['collection_id'] as int);
}
return result;
}
Future<void> updateDevicePathSyncStatus(
Map<String, bool> syncStatus,
) async {
final db = await sqliteAsyncDB;
int batchCounter = 0;
final parameterSets = <List<Object?>>[];
for (MapEntry e in syncStatus.entries) {
final String pathID = e.key;
parameterSets.add([e.value ? _sqlBoolTrue : _sqlBoolFalse, pathID]);
batchCounter++;
if (batchCounter == 400) {
await db.executeBatch(
'''
UPDATE device_collections SET should_backup = ? WHERE id = ?;
''',
parameterSets,
);
parameterSets.clear();
batchCounter = 0;
}
}
await db.executeBatch(
'''
UPDATE device_collections SET should_backup = ? WHERE id = ?;
''',
parameterSets,
);
}
Future<void> updateDeviceCollection(
String pathID,
int collectionID,
) async {
final db = await sqliteAsyncDB;
await db.execute(
'''
UPDATE device_collections SET collection_id = ? WHERE id = ?;
''',
[collectionID, pathID],
);
return;
}
Future<FileLoadResult> getFilesInDeviceCollection(
DeviceCollection deviceCollection,
int? ownerID,
int startTime,
int endTime, {
int? limit,
bool? asc,
}) async {
final db = await sqliteAsyncDB;
final order = (asc ?? false ? 'ASC' : 'DESC');
final String rawQuery = '''
SELECT *
FROM ${FilesDB.filesTable}
WHERE ${FilesDB.columnLocalID} IS NOT NULL AND
${FilesDB.columnCreationTime} >= $startTime AND
${FilesDB.columnCreationTime} <= $endTime AND
(${FilesDB.columnOwnerID} IS NULL OR ${FilesDB.columnOwnerID} =
$ownerID ) AND
${FilesDB.columnLocalID} IN
(SELECT id FROM device_files where path_id = '${deviceCollection.id}' )
ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order
''' +
(limit != null ? ' limit $limit;' : ';');
final results = await db.getAll(rawQuery);
final files = convertToFiles(results);
final dedupe = deduplicateByLocalID(files);
return FileLoadResult(dedupe, files.length == limit);
}
Future<BackedUpFileIDs> getBackedUpForDeviceCollection(
String pathID,
int ownerID,
) async {
final db = await sqliteAsyncDB;
const String rawQuery = '''
SELECT ${FilesDB.columnLocalID}, ${FilesDB.columnUploadedFileID},
${FilesDB.columnFileSize}
FROM ${FilesDB.filesTable}
WHERE ${FilesDB.columnLocalID} IS NOT NULL AND
(${FilesDB.columnOwnerID} IS NULL OR ${FilesDB.columnOwnerID} = ?)
AND (${FilesDB.columnUploadedFileID} IS NOT NULL AND ${FilesDB.columnUploadedFileID} IS NOT -1)
AND
${FilesDB.columnLocalID} IN
(SELECT id FROM device_files where path_id = ?)
''';
final results = await db.getAll(rawQuery, [ownerID, pathID]);
final localIDs = <String>{};
final uploadedIDs = <int>{};
int localSize = 0;
for (final result in results) {
final String localID = result[FilesDB.columnLocalID] as String;
final int? fileSize = result[FilesDB.columnFileSize] as int?;
if (!localIDs.contains(localID) && fileSize != null) {
localSize += fileSize;
}
localIDs.add(localID);
uploadedIDs.add(result[FilesDB.columnUploadedFileID] as int);
}
return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList(), localSize);
}
Future<List<DeviceCollection>> getDeviceCollections({
bool includeCoverThumbnail = false,
}) async {
debugPrint(
"Fetching DeviceCollections From DB with thumbnail = "
"$includeCoverThumbnail",
);
try {
final db = await sqliteAsyncDB;
final coverFiles = <EnteFile>[];
if (includeCoverThumbnail) {
final fileRows = await db.getAll(
'''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id;
''',
);
final files = convertToFiles(fileRows);
coverFiles.addAll(files);
}
final deviceCollectionRows = await db.getAll(
'''SELECT * from device_collections''',
);
final List<DeviceCollection> deviceCollections = [];
for (var row in deviceCollectionRows) {
final DeviceCollection deviceCollection = DeviceCollection(
row["id"] as String,
(row['name'] ?? '') as String,
count: row['count'] as int,
collectionID: (row["collection_id"] ?? -1) as int,
coverId: row["cover_id"] as String?,
shouldBackup: (row["should_backup"] ?? _sqlBoolFalse) == _sqlBoolTrue,
uploadStrategy: getUploadType((row["upload_strategy"] ?? 0) as int),
);
if (includeCoverThumbnail) {
deviceCollection.thumbnail = coverFiles.firstWhereOrNull(
(element) => element.localID == deviceCollection.coverId,
);
if (deviceCollection.thumbnail == null) {
final EnteFile? result =
await getDeviceCollectionThumbnail(deviceCollection.id);
if (result == null) {
_logger.info(
'Failed to find coverThumbnail for deviceFolder',
);
continue;
} else {
deviceCollection.thumbnail = result;
}
}
}
deviceCollections.add(deviceCollection);
}
if (includeCoverThumbnail) {
deviceCollections.sort(
(a, b) =>
b.thumbnail!.creationTime!.compareTo(a.thumbnail!.creationTime!),
);
}
return deviceCollections;
} catch (e) {
_logger.severe('Failed to getDeviceCollections', e);
rethrow;
}
}
Future<EnteFile?> getDeviceCollectionThumbnail(String pathID) async {
debugPrint("Call fallback method to get potential thumbnail");
final db = await sqliteAsyncDB;
final fileRows = await db.getAll(
'''SELECT * FROM FILES f JOIN device_files df on f.local_id = df.id
and df.path_id= ? order by f.creation_time DESC limit 1;
''',
[pathID],
);
final files = convertToFiles(fileRows);
if (files.isNotEmpty) {
return files.first;
} else {
return null;
}
}
Future<void> _insertBatch(
List<List<Object?>> parameterSets,
ConflictAlgorithm conflictAlgorithm,
) async {
final db = await sqliteAsyncDB;
await db.executeBatch(
'''
INSERT OR ${conflictAlgorithm.name.toUpperCase()}
INTO device_files (id, path_id) VALUES (?, ?);
''',
parameterSets,
);
}
Future<void> _deleteBatch(List<List<Object?>> parameterSets) async {
final db = await sqliteAsyncDB;
await db.executeBatch(
'''
DELETE FROM device_files WHERE id = ? AND path_id = ?;
''',
parameterSets,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
import "dart:io";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:path/path.dart";
import "package:path_provider/path_provider.dart";
import "package:photo_manager/photo_manager.dart";
import "package:photos/db/common/base.dart";
import "package:photos/db/local/mappers.dart";
import "package:photos/db/local/schema.dart";
import "package:photos/log/devlog.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/file/mapping/local_mapping.dart";
import "package:photos/models/local/local_metadata.dart";
import "package:sqlite_async/sqlite_async.dart";
class LocalDB with SqlDbBase {
static const _databaseName = "local_6.db";
static const batchInsertMaxCount = 1000;
static const _smallTableBatchInsertMaxCount = 5000;
late final SqliteDatabase _sqliteDB;
SqliteDatabase get sqliteDB => _sqliteDB;
Future<void> init() async {
devLog("LocalDB init");
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
final db = SqliteDatabase(path: path);
await migrate(db, LocalDBMigration.migrationScripts, onForeignKey: true);
_sqliteDB = db;
debugPrint("LocalDB init complete $path");
}
Future<void> insertAssets(List<AssetEntity> entries) async {
if (entries.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(entries.slices(batchInsertMaxCount), (slice) async {
final List<List<Object?>> values =
slice.map((e) => LocalDBMappers.assetsRow(e)).toList();
await _sqliteDB.executeBatch(
'INSERT INTO assets ($assetColumns) values(${getParams(16)}) ON CONFLICT(id) DO UPDATE SET $updateAssetColumns',
values,
);
});
debugPrint(
'$runtimeType insertAssets complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} assets',
);
}
// Store time and location metadata inside edited_assets
Future<void> trackEdit(
String id,
int createdAt,
int modifiedAt,
double? lat,
double? lng,
) async {
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'INSERT INTO edited_assets (id, created_at, modified_at, latitude, longitude) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET created_at = ?, modified_at = ?, latitude = ?, longitude = ?',
[id, createdAt, modifiedAt, lat, lng, createdAt, modifiedAt, lat, lng],
);
debugPrint(
'$runtimeType editCopy complete in ${stopwatch.elapsed.inMilliseconds}ms for $id',
);
}
Future<void> updateMetadata(
String id, {
DroidMetadata? droid,
IOSMetadata? ios,
}) async {
if (droid != null) {
await _sqliteDB.execute(
'UPDATE assets SET size = ?, hash = ?, latitude = ?, longitude = ?, created_at = ?, modified_at = ?, scan_state = 1 WHERE id = ?',
[
droid.size,
droid.hash,
droid.location?.latitude,
droid.location?.longitude,
droid.creationTime,
droid.modificationTime,
id,
],
);
} else if (ios != null) {
// await _sqliteDB.execute(
// 'UPDATE assets SET size = ?, hash = ?, latitude = ?, longitude = ?, created_at = ?, modified_at = ? WHERE id = ?',
// [
// ios.size,
// ios.hash,
// ios.location.latitude,
// ios.location.longitude,
// ios.creationTime.millisecondsSinceEpoch,
// ios.modificationTime.millisecondsSinceEpoch,
// ios.id,
// ],
// );
}
}
Future<Map<String, LocalAssetInfo>> getLocalAssetsInfo(
List<String> ids,
) async {
if (ids.isEmpty) return {};
final stopwatch = Stopwatch()..start();
final result = await _sqliteDB.getAll(
'SELECT id, hash, title, relative_path, scan_state FROM assets WHERE id IN (${List.filled(ids.length, "?").join(",")})',
ids,
);
debugPrint(
"getLocalAssetsInfo complete in ${stopwatch.elapsed.inMilliseconds}ms for ${ids.length} ids",
);
return Map.fromEntries(
result.map(
(row) => MapEntry(row['id'] as String, LocalAssetInfo.fromRow(row)),
),
);
}
Future<List<EnteFile>> getAssets({LocalAssertsParam? params}) async {
final stopwatch = Stopwatch()..start();
final result = await _sqliteDB.getAll(
"SELECT * FROM assets ${params != null ? params.whereClause(addWhere: true) : ""}",
);
debugPrint(
"getAssets complete in ${stopwatch.elapsed.inMilliseconds}ms, params: ${params?.whereClause()}",
);
// if time is greater than 1000ms, print explain analyze out
if (kDebugMode && stopwatch.elapsed.inMilliseconds > 1000) {
final explain = await _sqliteDB.execute(
"EXPLAIN QUERY PLAN SELECT * FROM assets ${params != null ? params.whereClause(addWhere: true) : ""}",
);
debugPrint("getAssets: Explain Query Plan: $explain");
}
stopwatch.reset();
stopwatch.start();
final r =
result.map((row) => LocalDBMappers.assetRowToEnteFile(row)).toList();
debugPrint(
"getAssets mapping completed in ${stopwatch.elapsed.inMilliseconds}ms",
);
return r;
}
Future<List<EnteFile>> getPathAssets(
String pathID, {
LocalAssertsParam? params,
}) async {
final String query =
"SELECT * FROM assets WHERE id IN (SELECT asset_id FROM device_path_assets WHERE path_id = ?) ${params != null ? 'AND ${params.whereClause()}' : "order by created_at desc"}";
debugPrint(query);
final result = await _sqliteDB.getAll(
query,
[pathID],
);
return result.map((row) => LocalDBMappers.assetRowToEnteFile(row)).toList();
}
Future<void> insertDBPaths(List<AssetPathEntity> entries) async {
if (entries.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(entries.slices(_smallTableBatchInsertMaxCount),
(slice) async {
final List<List<Object?>> values =
slice.map((e) => LocalDBMappers.devicePathRow(e)).toList();
await _sqliteDB.executeBatch(
'INSERT INTO device_path ($devicePathColumns) values(${getParams(5)}) ON CONFLICT(path_id) DO UPDATE SET $updateDevicePathColumns',
values,
);
});
debugPrint(
'$runtimeType insertDBPaths complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} paths',
);
}
Future<List<AssetPathEntity>> getAssetPaths() async {
final result = await _sqliteDB.getAll(
"SELECT * FROM device_path",
);
return result.map((row) => LocalDBMappers.assetPath(row)).toList();
}
Future<void> insertPathToAssetIDs(
Map<String, Set<String>> pathToAssetIDs, {
bool clearOldMappingsIdsInInput = false,
}) async {
if (pathToAssetIDs.isEmpty) return;
final List<List<String>> allValues = [];
pathToAssetIDs.forEach((pathID, assetIDs) {
allValues.addAll(assetIDs.map((assetID) => [pathID, assetID]));
});
if (allValues.isEmpty && !clearOldMappingsIdsInInput) {
return;
}
final stopwatch = Stopwatch()..start();
await _sqliteDB.writeTransaction((tx) async {
if (clearOldMappingsIdsInInput) {
await tx.execute(
"DELETE FROM device_path_assets WHERE path_id IN (${List.generate(pathToAssetIDs.keys.length, (index) => '?').join(',')})",
pathToAssetIDs.keys.toList(),
);
}
const int batchSize = 15000;
for (int i = 0; i < allValues.length; i += batchSize) {
await tx.executeBatch(
'INSERT OR REPLACE INTO device_path_assets (path_id, asset_id) VALUES (?, ?)',
allValues.sublist(
i,
i + batchSize > allValues.length ? allValues.length : i + batchSize,
),
);
}
});
debugPrint(
'$runtimeType insertPathToAssetIDs ${allValues.length} complete in '
'${stopwatch.elapsed.inMilliseconds}ms for '
'${pathToAssetIDs.length} paths (replaced $clearOldMappingsIdsInInput}',
);
}
Future<Set<String>> getAssetsIDs({bool pendingScan = false}) async {
final result = await _sqliteDB.getAll(
"SELECT id FROM assets ${pendingScan ? 'WHERE scan_state != $finalState ORDER BY created_at DESC' : ''}",
);
final ids = <String>{};
for (var row in result) {
ids.add(row["id"] as String);
}
return ids;
}
Future<Set<String>> getAssetsIDsForPath(
String pathID,
) async {
final result = await _sqliteDB.getAll(
"SELECT asset_id FROM device_path_assets WHERE path_id = ? ",
[pathID],
);
final ids = <String>{};
for (var row in result) {
ids.add(row["asset_id"] as String);
}
return ids;
}
Future<Map<String, int>> getIDToCreationTime() async {
final result = await _sqliteDB.getAll(
"SELECT id, created_at FROM assets",
);
final idToCreationTime = <String, int>{};
for (var row in result) {
idToCreationTime[row["id"] as String] = row["created_at"] as int;
}
return idToCreationTime;
}
Future<Map<String, Set<String>>> pathToAssetIDs() async {
final result = await _sqliteDB
.getAll("SELECT path_id, asset_id FROM device_path_assets");
final pathToAssetIDs = <String, Set<String>>{};
for (var row in result) {
final pathID = row["path_id"] as String;
final assetID = row["asset_id"] as String;
if (pathToAssetIDs.containsKey(pathID)) {
pathToAssetIDs[pathID]!.add(assetID);
} else {
pathToAssetIDs[pathID] = {assetID};
}
}
return pathToAssetIDs;
}
Future<void> deleteAssets(Set<String> ids) async {
if (ids.isEmpty) return;
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'DELETE FROM assets WHERE id IN (${List.filled(ids.length, "?").join(",")})',
ids.toList(),
);
debugPrint(
'$runtimeType deleteEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${ids.length} assets entries',
);
}
Future<void> deletePaths(Set<String> pathIds) async {
if (pathIds.isEmpty) return;
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'DELETE FROM device_path WHERE path_id IN (${List.filled(pathIds.length, "?").join(",")})',
pathIds.toList(),
);
debugPrint(
'$runtimeType deleteEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${pathIds.length} path entries',
);
}
// returns true if either asset queue or shared_assets has any entry for given ownerID
Future<bool> hasAssetQueueOrSharedAsset(int ownerID) async {
final result = await _sqliteDB.getAll(
'''
SELECT 1 FROM asset_upload_queue WHERE owner_id = ?
UNION ALL
SELECT 1 FROM shared_assets WHERE owner_id = ?
LIMIT 1
''',
[ownerID, ownerID],
);
return result.isNotEmpty;
}
Future<(int, int)> getUniqueQueueAndSharedAssetsCount(
int ownerID,
) async {
final queuedAssets = await _sqliteDB.getAll(
'SELECT COUNT(distinct asset_id) as count FROM asset_upload_queue WHERE owner_id = ?',
[ownerID],
);
final sharedAssets = await _sqliteDB.getAll(
'SELECT COUNT(*) as count FROM shared_assets WHERE owner_id = ?',
[ownerID],
);
final queuedCount =
queuedAssets.isNotEmpty ? (queuedAssets.first['count'] as int) : 0;
final sharedCount =
sharedAssets.isNotEmpty ? (sharedAssets.first['count'] as int) : 0;
return (queuedCount, sharedCount);
}
}

View File

@@ -0,0 +1,100 @@
import "dart:io";
import "package:photo_manager/photo_manager.dart";
import "package:photos/models/file/file.dart";
class LocalDBMappers {
const LocalDBMappers._();
static List<Object?> assetsRow(AssetEntity entity) {
return [
entity.id,
entity.type.index,
entity.subtype,
entity.width,
entity.height,
entity.duration,
entity.orientation,
entity.isFavorite ? 1 : 0,
entity.title,
entity.relativePath,
entity.createDateTime.microsecondsSinceEpoch,
entity.modifiedDateTime.microsecondsSinceEpoch,
entity.mimeType,
entity.latitude,
entity.longitude,
0, // scan_state
];
}
static AssetEntity asset(Map<String, dynamic> row) {
return AssetEntity(
id: row['id'] as String,
typeInt: row['type'] as int,
subtype: row['sub_type'] as int,
width: row['width'] as int,
height: row['height'] as int,
duration: row['duration_in_sec'] as int,
orientation: row['orientation'] as int,
isFavorite: (row['is_fav'] as int) == 1,
title: row['title'] as String?,
relativePath: row['relative_path'] as String?,
createDateSecond: (row['created_at'] as int) ~/ 1000000,
modifiedDateSecond: (row['modified_at'] as int) ~/ 1000000,
mimeType: row['mime_type'] as String?,
latitude: row['latitude'] as double?,
longitude: row['longitude'] as double?,
);
}
static EnteFile assetRowToEnteFile(Map<String, dynamic> row) {
final asset = AssetEntity(
id: row['id'] as String,
typeInt: row['type'] as int,
subtype: row['sub_type'] as int,
width: row['width'] as int,
height: row['height'] as int,
duration: row['duration_in_sec'] as int,
orientation: row['orientation'] as int,
isFavorite: (row['is_fav'] as int) == 1,
title: row['title'] as String?,
relativePath: row['relative_path'] as String?,
createDateSecond: (row['created_at'] as int) ~/ 1000000,
modifiedDateSecond: (row['modified_at'] as int) ~/ 1000000,
mimeType: row['mime_type'] as String?,
latitude: row['latitude'] as double?,
longitude: row['longitude'] as double?,
);
return EnteFile.fromAssetSync(asset);
}
static List<Object?> devicePathRow(AssetPathEntity entity) {
return [
entity.id,
entity.name,
entity.albumType,
entity.albumTypeEx?.darwin?.type?.index,
entity.albumTypeEx?.darwin?.subtype?.index,
];
}
static AssetPathEntity assetPath(Map<String, dynamic> row) {
return AssetPathEntity(
id: row['path_id'] as String,
name: row['name'] as String,
albumType: row['album_type'] as int,
albumTypeEx: AlbumType(
darwin: !Platform.isAndroid
? DarwinAlbumType(
type: PMDarwinAssetCollectionTypeExt.fromValue(
row['ios_album_type'] as int?,
),
subtype: PMDarwinAssetCollectionSubtypeExt.fromValue(
row['darwin_subtype'] as int?,
),
)
: null,
),
);
}
}

View File

@@ -0,0 +1,253 @@
import "dart:io";
import "package:flutter/foundation.dart";
import "package:sqlite_async/sqlite_async.dart";
const assetColumns =
"id, type, sub_type, width, height, duration_in_sec, orientation, is_fav, title, relative_path, created_at, modified_at, mime_type, latitude, longitude, scan_state";
const assetUploadQueueColumns =
"dest_collection_id, asset_id, path_id, owner_id, manual";
const androidAssetState = 1;
const androidHashState = 1 << 2;
const androidMediaType = 1 << 3;
const iOSAssetState = 1;
const iOSCloudIdState = 1 << 2;
const iOSAssetHashState = 1 << 3;
final finalState = Platform.isAndroid
? (androidAssetState ^ androidHashState ^ androidMediaType)
: (iOSAssetState ^ iOSCloudIdState ^ iOSAssetHashState);
// Generate the update clause dynamically (excludes 'id')
final String updateAssetColumns = assetColumns
.split(', ')
.where((column) => column != 'id') // Exclude primary key from update
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const devicePathColumns =
"path_id, name, album_type, ios_album_type, ios_album_subtype";
final String updateDevicePathColumns = devicePathColumns
.split(', ')
.where((column) => column != 'path_id') // Exclude primary key from update
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const String deviceCollectionWithOneAssetQuery = '''
WITH latest_per_path AS (
SELECT
dpa.path_id,
MAX(a.created_at) as max_created,
count(*) as asset_count
FROM
device_path_assets dpa
JOIN
assets a ON dpa.asset_id = a.id
GROUP BY
dpa.path_id
),
ranked_assets AS (
SELECT
dpa.path_id,
a.*,
ROW_NUMBER() OVER (PARTITION BY dpa.path_id ORDER BY a.id) as rn,
lpp.asset_count
FROM
device_path_assets dpa
JOIN
assets a ON dpa.asset_id = a.id
JOIN
latest_per_path lpp ON dpa.path_id = lpp.path_id AND a.created_at = lpp.max_created
)
SELECT
dp.*,
ra.*,
pc.*
FROM
device_path dp
JOIN
ranked_assets ra ON dp.path_id = ra.path_id AND ra.rn = 1
LEFT JOIN path_backup_config pc
on dp.path_id = pc.device_path_id
''';
class LocalAssertsParam {
int? limit;
int? offset;
String? orderByColumn;
bool isAsc;
(int?, int?)? createAtRange;
LocalAssertsParam({
this.limit,
this.offset,
this.orderByColumn = "created_at",
this.isAsc = false,
this.createAtRange,
});
String get orderBy => orderByColumn == null
? ""
: "ORDER BY $orderByColumn ${isAsc ? "ASC" : "DESC"}";
String get limitOffset => (limit != null && offset != null)
? "LIMIT $limit + OFFSET $offset)"
: (limit != null)
? "LIMIT $limit"
: "";
String get createAtRangeStr => (createAtRange == null ||
createAtRange!.$1 == null)
? ""
: "(created_at BETWEEN ${createAtRange!.$1} AND ${createAtRange!.$2})";
String whereClause({bool addWhere = false}) {
final where = <String>[];
if (createAtRangeStr.isNotEmpty) {
where.add(createAtRangeStr);
}
return (where.isEmpty
? ""
: '${addWhere ? "Where" : ""} ${where.join(" AND ")}') +
" " +
orderBy +
" " +
limitOffset;
}
}
class LocalDBMigration {
static const migrationScripts = [
'''
CREATE TABLE assets (
id TEXT PRIMARY KEY,
type INTEGER NOT NULL,
sub_type INTEGER NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
duration_in_sec INTEGER NOT NULL,
orientation INTEGER NOT NULL,
is_fav INTEGER NOT NULL,
title TEXT NOT NULL,
relative_path TEXT,
created_at INTEGER NOT NULL,
modified_at INTEGER NOT NULL,
mime_type TEXT,
latitude REAL,
longitude REAL,
scan_state INTEGER DEFAULT 0,
hash TEXT,
size INTEGER,
os_metadata TEXT DEFAULT '{}'
);
''',
'''
CREATE INDEX IF NOT EXISTS assets_created_at ON assets(created_at);
''',
'''
CREATE TABLE shared_assets (
dest_collection_id INTEGER NOT NULL,
id TEXT NOT NULL,
name TEXT NOT NULL,
type INTEGER NOT NULL,
created_at INTEGER NOT NULL,
duration_in_sec INTEGER DEFAULT 0,
owner_id INTEGER NOT NULL,
latitude REAL,
longitude REAL,
PRIMARY KEY (dest_collection_id, id)
);
''',
'''
CREATE INDEX IF NOT EXISTS sa_collection_owner ON shared_assets(dest_collection_id, owner_id);
''',
'''
CREATE TABLE device_path (
path_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
album_type INTEGER NOT NULL,
ios_album_type INTEGER,
ios_album_subtype INTEGER
);
''',
'''
CREATE TABLE device_path_assets (
path_id TEXT NOT NULL,
asset_id TEXT NOT NULL,
PRIMARY KEY (path_id, asset_id),
FOREIGN KEY (path_id) REFERENCES device_path(path_id) ON DELETE CASCADE,
FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE CASCADE
);
''',
'''
CREATE TABLE queue (
id TEXT NOT NULL,
name TEXT NOT NULL,
PRIMARY KEY (id, name)
);
''',
'''
CREATE TABLE path_backup_config(
device_path_id TEXT PRIMARY KEY,
owner_id INTEGER NOT NULL,
collection_id INTEGER,
should_backup INTEGER NOT NULL DEFAULT 0,
upload_strategy INTEGER NOT NULL DEFAULT 0
);
''',
'''
CREATE TABLE asset_upload_queue (
dest_collection_id INTEGER NOT NULL,
asset_id TEXT NOT NULL,
path_id TEXT,
owner_id INTEGER NOT NULL,
manual INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (dest_collection_id, asset_id),
FOREIGN KEY(asset_id) REFERENCES assets(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_asset_upload_queue_owner_id
ON asset_upload_queue(owner_id)
WHERE owner_id IS NOT NULL;
''',
'''
CREATE INDEX IF NOT EXISTS assets_created_at_desc ON assets(created_at DESC);
''',
'''
CREATE TABLE edited_assets (
id String NOT NULL,
created_at INTEGER NOT NULL,
modified_at INTEGER NOT NULL,
latitude REAL,
longitude REAL,
PRIMARY KEY (id)
FOREIGN KEY (id) REFERENCES assets(id) ON DELETE CASCADE
);
''',
];
static Future<void> migrate(
SqliteDatabase database,
) async {
final result = await database.execute('PRAGMA user_version');
await database.execute("PRAGMA foreign_keys = ON");
final currentVersion = result[0]['user_version'] as int;
final toVersion = migrationScripts.length;
if (currentVersion < toVersion) {
debugPrint("Migrating Local DB from $currentVersion to $toVersion");
await database.writeTransaction((tx) async {
for (int i = currentVersion + 1; i <= toVersion; i++) {
await tx.execute(migrationScripts[i - 1]);
}
await tx.execute('PRAGMA user_version = $toVersion');
});
} else if (currentVersion > toVersion) {
throw AssertionError(
"currentVersion($currentVersion) cannot be greater than toVersion($toVersion)",
);
}
}
}

View File

@@ -0,0 +1,33 @@
import "package:photo_manager/photo_manager.dart";
import "package:photos/db/local/db.dart";
import "package:photos/db/local/mappers.dart";
import "package:photos/db/local/schema.dart";
import "package:photos/models/device_collection.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/upload_strategy.dart";
extension DeviceAlbums on LocalDB {
Future<List<DeviceCollection>> getDeviceCollections() async {
final List<DeviceCollection> collections = [];
final rows = await sqliteDB.getAll(deviceCollectionWithOneAssetQuery);
for (final row in rows) {
final path = LocalDBMappers.assetPath(row);
AssetEntity? asset;
if (row['id'] != null) {
asset = LocalDBMappers.asset(row);
}
collections.add(
DeviceCollection(
path,
count: row['asset_count'] as int,
thumbnail: asset != null ? EnteFile.fromAssetSync(asset) : null,
shouldBackup: (row['should_backup'] ?? 0) as int == 1,
uploadStrategy:
UploadStrategy.values[(row['upload_strategy'] ?? 0) as int],
),
);
}
return collections;
}
}

View File

@@ -0,0 +1,92 @@
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:photos/db/local/db.dart";
import "package:photos/log/devlog.dart";
import "package:photos/models/local/path_config.dart";
import "package:photos/models/upload_strategy.dart";
extension PathBackupConfigTable on LocalDB {
Future<void> insertOrUpdatePathConfigs(
Map<String, bool> pathConfigs,
int ownerID,
) async {
if (pathConfigs.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(
pathConfigs.entries.slices(LocalDB.batchInsertMaxCount), (slice) async {
final List<List<Object?>> values =
slice.map((e) => [e.key, e.value ? 1 : 0, ownerID]).toList();
await sqliteDB.executeBatch(
'INSERT INTO path_backup_config (device_path_id, should_backup, owner_id) VALUES (?, ?, ?) ON CONFLICT(device_path_id) DO UPDATE SET should_backup = ?, owner_id = ?',
values.map((e) => [e[0], e[1], e[2], e[1], e[2]]).toList(),
);
});
debugPrint(
'$runtimeType insertOrUpdatePathConfigs complete in ${stopwatch.elapsed.inMilliseconds}ms for ${pathConfigs.length} paths',
);
}
Future<Set<String>> getBackedUpPathIDs(int ownerID) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT device_path_id FROM path_backup_config WHERE should_backup = 1 AND owner_id = ?',
[ownerID],
);
final paths = result.map((row) => row['device_path_id'] as String).toSet();
devLog(
'$runtimeType getPathsWithBackupEnabled complete in ${stopwatch.elapsed.inMilliseconds}ms',
name: 'getPathsWithBackupEnabled',
);
return paths;
}
// destCollectionWithBackup returns the non-null collection ids
// for given ownerID for paths that have backup enabled.
Future<Set<int>> destCollectionWithBackup(int ownerID) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT collection_id FROM path_backup_config WHERE should_backup = 1 AND owner_id = ? AND collection_id IS NOT NULL',
[ownerID],
);
final Set<int> collectionIDs =
result.map((row) => row['collection_id'] as int).whereNotNull().toSet();
devLog(
'$runtimeType destCollectionWithBackup complete in ${stopwatch.elapsed.inMilliseconds}ms',
name: 'destCollectionWithBackup',
);
return collectionIDs;
}
Future<void> updateDestConnection(
String pathID,
int destCollection,
int ownerID,
) async {
await sqliteDB.execute(
'UPDATE path_backup_config SET collection_id = ? WHERE device_path_id = ? AND owner_id = ?',
[destCollection, pathID, ownerID],
);
}
Future<List<PathConfig>> getPathConfigs(int ownerID) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT * FROM path_backup_config WHERE owner_id = ?',
[ownerID],
);
final configs = result.map((row) {
return PathConfig(
row['device_path_id'] as String,
row['owner_id'] as int,
row['collection_id'] as int?,
(row['should_backup'] as int) == 1,
getUploadType(row['upload_strategy'] as int),
);
}).toList();
devLog(
'$runtimeType getPathConfigs complete in ${stopwatch.elapsed.inMilliseconds}ms',
name: 'getPathConfigs',
);
return configs;
}
}

View File

@@ -0,0 +1,69 @@
import "package:collection/collection.dart";
import "package:photos/db/local/db.dart";
import "package:photos/models/local/shared_asset.dart";
extension SharedAssetsTable on LocalDB {
Future<Set<String>> getSharedAssetsID() async {
final result = await sqliteDB.getAll('SELECT id FROM shared_assets');
return Set.unmodifiable(result.map<String>((row) => row['id'] as String));
}
Future<void> insertSharedAssets(List<SharedAsset> assets) async {
if (assets.isEmpty) return;
await Future.forEach(
assets.slices(LocalDB.batchInsertMaxCount),
(slice) async {
final List<List<Object?>> values =
slice.map((e) => e.rowProps).toList();
await sqliteDB.executeBatch(
'INSERT INTO shared_assets (id, name, type, creation_time, duration_in_seconds, dest_collection_id, owner_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
values,
);
},
);
}
Future<List<SharedAsset>> getSharedAssets() async {
final result = await sqliteDB.getAll(
'SELECT * FROM shared_assets ORDER BY creation_time DESC',
);
return result.map((row) => SharedAsset.fromRow(row)).toList();
}
Future<List<SharedAsset>> getSharedAssetsByCollection(
int collectionID,
) async {
final result = await sqliteDB.getAll(
'SELECT * FROM shared_assets WHERE dest_collection_id = ? ORDER BY creation_time DESC',
[collectionID],
);
return result.map((row) => SharedAsset.fromRow(row)).toList();
}
Future<void> deleteSharedAssetsByCollection(int collectionID) async {
await sqliteDB.execute(
'DELETE FROM shared_assets WHERE dest_collection_id = ?',
[collectionID],
);
}
Future<void> deleteSharedAsset(String assetID) async {
await sqliteDB.execute(
'DELETE FROM shared_assets WHERE id = ?',
[assetID],
);
}
Future<void> deleteSharedAssets(Set<String> assetIDs) async {
if (assetIDs.isEmpty) return;
await Future.forEach(
assetIDs.slices(LocalDB.batchInsertMaxCount),
(slice) async {
await sqliteDB.executeBatch(
'DELETE FROM shared_assets WHERE id = ?',
slice.map((id) => [id]).toList(),
);
},
);
}
}

View File

@@ -0,0 +1,148 @@
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:photos/db/local/db.dart";
import "package:photos/db/local/mappers.dart";
import "package:photos/db/local/schema.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/local/asset_upload_queue.dart";
extension UploadQueueTable on LocalDB {
Future<Set<String>> getQueueAssetIDs(int ownerID) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT asset_id FROM asset_upload_queue WHERE owner_id = ?',
[ownerID],
);
final assetIDs = result.map((row) => row['asset_id'] as String).toSet();
debugPrint(
'$runtimeType getQueueAssetIDs complete in ${stopwatch.elapsed.inMilliseconds}ms',
);
return assetIDs;
}
Future<void> clearMappingsWithDiffPath(
int ownerID,
Set<String> pathIDs,
) async {
if (pathIDs.isEmpty) {
// delete all mapping with path ids
await sqliteDB.execute(
'DELETE FROM asset_upload_queue WHERE owner_id = ? AND path_id IS NOT NULL',
[ownerID],
);
} else {
// delete mappings where path_id is not null and not in pathIDs
final stopwatch = Stopwatch()..start();
await sqliteDB.execute(
'DELETE FROM asset_upload_queue WHERE owner_id = ? AND path_id IS NOT NULL AND path_id NOT IN (${pathIDs.map((_) => '?').join(',')})',
[ownerID, ...pathIDs],
);
debugPrint(
'$runtimeType clearMappingsWithDiffPath complete in ${stopwatch.elapsed.inMilliseconds}ms for ${pathIDs.length} paths',
);
}
}
Future<bool> existsQueueEntry(AssetUploadQueue entry) async {
final result = await sqliteDB.getAll(
'SELECT 1 FROM asset_upload_queue WHERE asset_id = ? AND owner_id = ? AND dest_collection_id = ?',
[entry.id, entry.ownerId, entry.destCollectionId],
);
return result.isNotEmpty;
}
Future<int> delete(AssetUploadQueue entry) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.execute(
'DELETE FROM asset_upload_queue WHERE asset_id = ? AND owner_id = ? and dest_collection_id = ?',
[entry.id, entry.ownerId, entry.destCollectionId],
);
debugPrint(
'$runtimeType delete complete in ${stopwatch.elapsed.inMilliseconds}ms for entry: $entry',
);
return result.isNotEmpty ? result[0]['changes'] as int : 0;
}
Future<List<(AssetUploadQueue, EnteFile)>> getQueueEntriesWithFiles(
int ownerID, {
int? destCollection,
}) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT asset_upload_queue.*, assets.* FROM asset_upload_queue JOIN assets ON assets.id = asset_upload_queue.asset_id WHERE owner_id = ? ${destCollection != null ? 'AND dest_collection_id = ?' : ''} ORDER BY created_at DESC',
destCollection != null ? [ownerID, destCollection] : [ownerID],
);
final entries = result
.map(
(row) => (
AssetUploadQueue(
id: row['asset_id'] as String,
pathId: row['path_id'] as String?,
destCollectionId: row['dest_collection_id'] as int,
ownerId: row['owner_id'] as int,
manual: (row['manual'] as int) == 1,
),
EnteFile.fromAssetSync(LocalDBMappers.asset(row)),
),
)
.toList();
debugPrint(
'$runtimeType getQueueEntriesWithFiles complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries',
);
return entries;
}
Future<List<AssetUploadQueue>> getQueueEntries(
int ownerID, {
int? destCollection,
}) async {
final stopwatch = Stopwatch()..start();
final result = await sqliteDB.getAll(
'SELECT asset_upload_queue.*, assets.* FROM asset_upload_queue JOIN assets ON assets.id = asset_upload_queue.asset_id WHERE owner_id = ? ${destCollection != null ? 'AND dest_collection_id = ?' : ''} ORDER BY created_at DESC',
destCollection != null ? [ownerID, destCollection] : [ownerID],
);
final entries = result
.map(
(row) => AssetUploadQueue(
id: row['asset_id'] as String,
pathId: row['path_id'] as String?,
destCollectionId: row['dest_collection_id'] as int,
ownerId: row['owner_id'] as int,
manual: (row['manual'] as int) == 1,
),
)
.toList();
debugPrint(
'$runtimeType getQueueEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries',
);
return entries;
}
Future<void> insertOrUpdateQueue(
Set<String> assetIDs,
int destCollection,
int ownerID, {
String? path,
bool manual = false,
}) async {
if (assetIDs.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(
assetIDs.slices(LocalDB.batchInsertMaxCount),
(slice) async {
final List<List<Object?>> values = slice
.map((e) => [destCollection, e, path, ownerID, manual])
.toList();
await sqliteDB.executeBatch(
'INSERT INTO asset_upload_queue ($assetUploadQueueColumns) VALUES(?,?,?,?,?) ON CONFLICT DO UPDATE SET manual = ?, path_id = ?',
values
.map((e) => [e[0], e[1], e[2], e[3], e[4], manual, path])
.toList(),
);
},
);
debugPrint(
'$runtimeType insertOrUpdateQueue complete in ${stopwatch.elapsed.inMilliseconds}ms for ${assetIDs.length} items',
);
}
}

View File

@@ -9,7 +9,7 @@ import "package:photos/services/machine_learning/face_ml/face_clustering/face_db
abstract class IMLDataDB<T> {
Future<void> bulkInsertFaces(List<Face> faces);
Future<void> updateFaceIdToClusterId(Map<String, String> faceIDToClusterID);
Future<Map<int, int>> faceIndexedFileIds({int minimumMlVersion});
Future<Map<T, int>> faceIndexedFileIds({int minimumMlVersion});
Future<int> getFaceIndexedFileCount({int minimumMlVersion});
Future<Map<String, int>> clusterIdToFaceCount();
Future<Set<String>> getPersonIgnoredClusters(String personID);
@@ -52,7 +52,7 @@ abstract class IMLDataDB<T> {
Future<void> forceUpdateClusterIds(Map<String, String> faceIDToClusterID);
Future<void> removeFaceIdToClusterId(Map<String, String> faceIDToClusterID);
Future<void> removePerson(String personID);
Future<List<FaceDbInfoForClustering>> getFaceInfoForClustering({
Future<List<FaceDbInfoForClustering<T>>> getFaceInfoForClustering({
int maxFaces,
int offset,
int batchSize,
@@ -112,9 +112,9 @@ abstract class IMLDataDB<T> {
});
Future<List<EmbeddingVector>> getAllClipVectors();
Future<Map<int, int>> clipIndexedFileWithVersion();
Future<Map<T, int>> clipIndexedFileWithVersion();
Future<int> getClipIndexedFileCount({int minimumMlVersion});
Future<void> putClip(List<ClipEmbedding> embeddings);
Future<void> putClip<T>(List<ClipEmbedding<T>> embeddings);
Future<void> deleteClipEmbeddings(List<T> fileIDs);
Future<void> deleteClipIndexes();
}

View File

@@ -72,25 +72,26 @@ class ClipVectorDB {
_migrationDone = true;
}
Future<void> insertEmbedding({
required int fileID,
Future<void> insertEmbedding<T>({
required T fileID,
required List<double> embedding,
}) async {
final db = await _vectorDB;
try {
await db.addVector(key: BigInt.from(fileID), vector: embedding);
final id = fileID as int;
await db.addVector(key: BigInt.from(id), vector: embedding);
} catch (e, s) {
_logger.severe("Error inserting embedding", e, s);
rethrow;
}
}
Future<void> bulkInsertEmbeddings({
required List<int> fileIDs,
Future<void> bulkInsertEmbeddings<T>({
required List<T> fileIDs,
required List<Float32List> embeddings,
}) async {
final db = await _vectorDB;
final bigKeys = Uint64List.fromList(fileIDs);
final bigKeys = Uint64List.fromList(fileIDs.map((e) => e as int).toList());
try {
await db.bulkAddVectors(keys: bigKeys, vectors: embeddings);
} catch (e, s) {

View File

@@ -53,13 +53,13 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
static final MLDataDB instance = MLDataDB._privateConstructor();
static final _migrationScripts = [
createFacesTable,
getCreateFacesTable(false),
createFaceClustersTable,
createClusterPersonTable,
createClusterSummaryTable,
createNotPersonFeedbackTable,
fcClusterIDIndex,
createClipEmbeddingsTable,
getCreateClipEmbeddingsTable(false),
createFileDataTable,
createFaceCacheTable,
];
@@ -80,10 +80,10 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
final asyncDBConnection =
SqliteDatabase(path: databaseDirectory, maxReaders: 2);
final stopwatch = Stopwatch()..start();
_logger.info("MLDataDB: Starting migration");
_logger.info("$runtimeType: Starting migration");
await migrate(asyncDBConnection, _migrationScripts);
_logger.info(
"MLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms",
"$runtimeType Migration took ${stopwatch.elapsedMilliseconds} ms",
);
stopwatch.stop();
@@ -360,10 +360,10 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
(element) => (element[fileIDColumn] as int) == avatarFileId,
);
if (row != null) {
return mapRowToFace(row);
return mapRowToFace<int>(row);
}
}
return mapRowToFace(faceMaps.first);
return mapRowToFace<int>(faceMaps.first);
}
}
if (clusterID != null) {
@@ -411,7 +411,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
if (maps.isEmpty) {
return null;
}
return maps.map((e) => mapRowToFace(e)).toList();
return maps.map((e) => mapRowToFace<int>(e)).toList();
}
@override
@@ -428,7 +428,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
}
final result = <int, List<FaceWithoutEmbedding>>{};
for (final map in maps) {
final face = mapRowToFaceWithoutEmbedding(map);
final face = mapRowToFaceWithoutEmbedding<int>(map);
final fileID = map[fileIDColumn] as int;
result.putIfAbsent(fileID, () => <FaceWithoutEmbedding>[]).add(face);
}
@@ -726,7 +726,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
}
@override
Future<List<FaceDbInfoForClustering>> getFaceInfoForClustering({
Future<List<FaceDbInfoForClustering<int>>> getFaceInfoForClustering({
int maxFaces = 20000,
int offset = 0,
int batchSize = 10000,
@@ -738,7 +738,8 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
);
final db = await instance.asyncDB;
final List<FaceDbInfoForClustering> result = <FaceDbInfoForClustering>[];
final List<FaceDbInfoForClustering<int>> result =
<FaceDbInfoForClustering<int>>[];
while (true) {
// Query a batch of rows
final List<Map<String, dynamic>> maps = await db.getAll(
@@ -758,7 +759,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
final faceIdToClusterId = await getFaceIdsToClusterIds(faceIds);
for (final map in maps) {
final faceID = map[faceIDColumn] as String;
final faceInfo = FaceDbInfoForClustering(
final faceInfo = FaceDbInfoForClustering<int>(
faceID: faceID,
clusterId: faceIdToClusterId[faceID],
embeddingBytes: map[embeddingColumn] as Uint8List,
@@ -1135,7 +1136,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
final db = await instance.asyncDB;
if (faces) {
await db.execute(deleteFacesTable);
await db.execute(createFacesTable);
await db.execute(getCreateFacesTable(false));
await db.execute(deleteFaceClustersTable);
await db.execute(createFaceClustersTable);
await db.execute(fcClusterIDIndex);
@@ -1335,8 +1336,8 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
"Got ${fileIDs.length} valid embeddings, $weirdCount weird embeddings",
);
await ClipVectorDB.instance
.bulkInsertEmbeddings(fileIDs: fileIDs, embeddings: embeddings);
await ClipVectorDB.instance.bulkInsertEmbeddings<int>(
fileIDs: fileIDs, embeddings: embeddings);
_logger.info("Inserted ${fileIDs.length} embeddings to ClipVectorDB");
processedCount += fileIDs.length;
offset += batchSize;
@@ -1396,7 +1397,7 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
}
@override
Future<void> putClip(List<ClipEmbedding> embeddings) async {
Future<void> putClip<int>(List<ClipEmbedding<int>> embeddings) async {
if (embeddings.isEmpty) return;
final db = await instance.asyncDB;
if (embeddings.length == 1) {
@@ -1406,8 +1407,9 @@ class MLDataDB with SqlDbBase implements IMLDataDB<int> {
);
if (flagService.enableVectorDb &&
await ClipVectorDB.instance.checkIfMigrationDone()) {
final e = embeddings.first.fileID;
await ClipVectorDB.instance.insertEmbedding(
fileID: embeddings.first.fileID,
fileID: e,
embedding: embeddings.first.embedding,
);
}

View File

@@ -7,7 +7,7 @@ import "package:photos/models/ml/face/face.dart";
import "package:photos/models/ml/face/face_with_embedding.dart";
import "package:photos/models/ml/ml_versions.dart";
Map<String, dynamic> mapRemoteToFaceDB(Face face) {
Map<String, dynamic> mapRemoteToFaceDB<T>(Face<T> face) {
return {
faceIDColumn: face.faceID,
fileIDColumn: face.fileID,
@@ -24,10 +24,10 @@ Map<String, dynamic> mapRemoteToFaceDB(Face face) {
};
}
Face mapRowToFace(Map<String, dynamic> row) {
Face mapRowToFace<T>(Map<String, dynamic> row) {
return Face(
row[faceIDColumn] as String,
row[fileIDColumn] as int,
row[fileIDColumn] as T,
EVector.fromBuffer(row[embeddingColumn] as List<int>).values,
row[faceScore] as double,
Detection.fromJson(json.decode(row[faceDetectionColumn] as String)),
@@ -39,10 +39,12 @@ Face mapRowToFace(Map<String, dynamic> row) {
);
}
FaceWithoutEmbedding mapRowToFaceWithoutEmbedding(Map<String, dynamic> row) {
return FaceWithoutEmbedding(
FaceWithoutEmbedding<T> mapRowToFaceWithoutEmbedding<T>(
Map<String, dynamic> row,
) {
return FaceWithoutEmbedding<T>(
row[faceIDColumn] as String,
row[fileIDColumn] as int,
row[fileIDColumn] as T,
row[faceScore] as double,
Detection.fromJson(json.decode(row[faceDetectionColumn] as String)),
row[faceBlur] as double,

File diff suppressed because it is too large Load Diff

View File

@@ -17,8 +17,9 @@ const personIdColumn = 'person_id';
const clusterIDColumn = 'cluster_id';
const personOrClusterIdColumn = 'person_or_cluster_id';
const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
$fileIDColumn INTEGER NOT NULL,
String getCreateFacesTable(bool isOfflineDB) {
return '''CREATE TABLE IF NOT EXISTS $facesTable (
$fileIDColumn ${isOfflineDB ? 'TEXT' : 'INTEGER'} NOT NULL,
$faceIDColumn TEXT NOT NULL UNIQUE,
$faceDetectionColumn TEXT NOT NULL,
$embeddingColumn BLOB NOT NULL,
@@ -31,6 +32,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable (
PRIMARY KEY($fileIDColumn, $faceIDColumn)
);
''';
}
const deleteFacesTable = 'DELETE FROM $facesTable';
// End of Faces Table Fields & Schema Queries
@@ -98,18 +100,20 @@ const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback';
// ## CLIP EMBEDDINGS TABLE
const clipTable = 'clip';
const createClipEmbeddingsTable = '''
CREATE TABLE IF NOT EXISTS $clipTable (
$fileIDColumn INTEGER NOT NULL,
String getCreateClipEmbeddingsTable(bool isOfflineDB) {
return '''CREATE TABLE IF NOT EXISTS $clipTable (
$fileIDColumn ${isOfflineDB ? 'TEXT' : 'INTEGER'} NOT NULL,
$embeddingColumn BLOB NOT NULL,
$mlVersionColumn INTEGER NOT NULL,
PRIMARY KEY ($fileIDColumn)
PRIMARY KEY($fileIDColumn)
);
''';
''';
}
const deleteClipEmbeddingsTable = 'DELETE FROM $clipTable';
const fileDataTable = 'filedata';
const createFileDataTable = '''
CREATE TABLE IF NOT EXISTS $fileDataTable (
$fileIDColumn INTEGER NOT NULL,

View File

@@ -0,0 +1,193 @@
import "dart:io";
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:path/path.dart";
import "package:path_provider/path_provider.dart";
import "package:photos/db/common/base.dart";
import "package:photos/db/remote/mappers.dart";
import "package:photos/db/remote/schema.dart";
import "package:photos/log/devlog.dart";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/collection/collection.dart";
import "package:photos/models/file/remote/asset.dart";
import "package:sqlite_async/sqlite_async.dart";
// ignore: constant_identifier_names
enum RemoteTable { collections, collection_files, files, entities, trash }
class RemoteDB with SqlDbBase {
static const _databaseName = "remotex6.db";
static const _batchInsertMaxCount = 1000;
late final SqliteDatabase _sqliteDB;
Future<void> init() async {
devLog("Starting RemoteDB init");
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
final db = SqliteDatabase(path: path);
await migrate(db, RemoteDBMigration.migrationScripts, onForeignKey: true);
_sqliteDB = db;
debugPrint("RemoteDB init complete $path");
}
SqliteDatabase get sqliteDB => _sqliteDB;
Future<List<Collection>> getAllCollections() async {
final result = <Collection>[];
final cursor = await _sqliteDB.getAll("SELECT * FROM collections");
for (final row in cursor) {
result.add(Collection.fromRow(row));
}
return result;
}
Future<void> clearAllTables() async {
final stopwatch = Stopwatch()..start();
await Future.wait([
_sqliteDB.execute('DELETE FROM collections'),
_sqliteDB.execute('DELETE FROM collection_files'),
_sqliteDB.execute('DELETE FROM files'),
_sqliteDB.execute('DELETE FROM files_metadata'),
_sqliteDB.execute('DELETE FROM trash'),
_sqliteDB.execute('DELETE FROM upload_mapping'),
]);
debugPrint(
'$runtimeType clearAllTables complete in ${stopwatch.elapsed.inMilliseconds}ms',
);
}
Future<Map<int, int>> getCollectionIDToUpdationTime() async {
final result = <int, int>{};
final cursor = await _sqliteDB.getAll(
"SELECT id, updation_time FROM collections where is_deleted = 0",
);
for (final row in cursor) {
result[row['id'] as int] = row['updation_time'] as int;
}
return result;
}
Future<List<RemoteAsset>> getRemoteAssets() async {
final result = <RemoteAsset>[];
final cursor = await _sqliteDB.getAll(
"SELECT * FROM files",
);
for (final row in cursor) {
result.add(fromFilesRow(row));
}
return result;
}
Future<void> insertCollections(List<Collection> collections) async {
if (collections.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(collections.slices(_batchInsertMaxCount),
(slice) async {
final List<List<Object?>> values =
slice.map((e) => e.rowValiues()).toList();
await _sqliteDB.executeBatch(
'INSERT INTO collections ($collectionColumns) values($collectionValuePlaceHolder) ON CONFLICT(id) DO UPDATE SET $updateCollectionColumns',
values,
);
});
debugPrint(
'$runtimeType insertCollections complete in ${stopwatch.elapsed.inMilliseconds}ms for ${collections.length} collections',
);
}
Future<List<RemoteAsset>> insertDiffItems(
List<DiffItem> items,
) async {
if (items.isEmpty) return [];
final List<RemoteAsset> assets = [];
final stopwatch = Stopwatch()..start();
await Future.forEach(items.slices(_batchInsertMaxCount), (slice) async {
final List<List<Object?>> collectionFileValues = [];
final List<List<Object?>> fileValues = [];
final List<List<Object?>> fileMetadataValues = [];
for (final item in slice) {
final rAsset = item.fileItem.toRemoteAsset();
collectionFileValues.add(item.collectionFileRowValues());
fileMetadataValues.add(item.fileItem.filesMetadataRowValues());
fileValues.add(remoteAssetToRow(rAsset));
assets.add(rAsset);
}
await Future.wait([
_sqliteDB.executeBatch(
'INSERT INTO collection_files ($collectionFilesColumns) values(?, ?, ?, ?, ?, ?) ON CONFLICT(file_id, collection_id) DO UPDATE SET $collectionFilesUpdateColumns',
collectionFileValues,
),
_sqliteDB.executeBatch(
'INSERT INTO files ($filesColumns) values(${getParams(23)}) ON CONFLICT(id) DO UPDATE SET $filesUpdateColumns',
fileValues,
),
_sqliteDB.executeBatch(
'INSERT INTO files_metadata ($filesMetadataColumns) values(${getParams(5)}) ON CONFLICT(id) DO UPDATE SET $filesMetadataUpdateColumns',
fileMetadataValues,
),
]);
});
debugPrint(
'$runtimeType insertCollectionFilesDiff complete in ${stopwatch.elapsed.inMilliseconds}ms for ${items.length}',
);
return assets;
}
Future<void> deleteFilesDiff(
List<DiffItem> items,
) async {
final int collectionID = items.first.collectionID;
final stopwatch = Stopwatch()..start();
await Future.forEach(items.slices(_batchInsertMaxCount), (slice) async {
await _sqliteDB.execute(
'DELETE FROM collection_files WHERE file_id IN (${slice.map((e) => e.fileID).join(',')}) AND collection_id = $collectionID',
);
});
debugPrint(
'$runtimeType deleteCollectionFilesDiff complete in ${stopwatch.elapsed.inMilliseconds}ms for ${items.length}',
);
}
Future<void> deleteEntries<T>(Set<T> ids, RemoteTable table) async {
if (ids.isEmpty) return;
final stopwatch = Stopwatch()..start();
await _sqliteDB.execute(
'DELETE FROM ${table.name.toLowerCase()} WHERE id IN (${ids.join(',')})',
);
debugPrint(
'$runtimeType deleteEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${ids.length} $table entries',
);
}
Future<int> rowCount(
RemoteTable table,
) async {
final row = await _sqliteDB.get(
'SELECT COUNT(*) as count FROM ${table.name}',
);
return row['count'] as int;
}
Future<Set<T>> _getByIds<T>(
Set<int> ids,
String table,
T Function(
Map<String, Object?> row,
) mapRow, {
String columnName = "id",
}) async {
final result = <T>{};
if (ids.isNotEmpty) {
final rows = await _sqliteDB.getAll(
'SELECT * from $table where $columnName IN (${ids.join(',')})',
);
for (final row in rows) {
result.add(mapRow(row));
}
}
return result;
}
}

View File

@@ -0,0 +1,114 @@
import "dart:typed_data";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/api/diff/trash_time.dart";
import "package:photos/models/file/file.dart";
import "package:photos/models/file/remote/asset.dart";
import "package:photos/models/file/remote/collection_file.dart";
import "package:photos/models/file/remote/rl_mapping.dart";
import "package:photos/models/location/location.dart";
RemoteAsset fromTrashRow(Map<String, dynamic> row) {
final metadata = Metadata.fromEncodedJson(row['metadata']);
final privateMetadata = Metadata.fromEncodedJson(row['priv_metadata']);
final publicMetadata = Metadata.fromEncodedJson(row['pub_metadata']);
final info = Info.fromEncodedJson(row['info']);
return RemoteAsset.fromMetadata(
id: row['id'],
ownerID: row['owner_id'],
thumbHeader: row['thumb_header'],
fileHeader: row['file_header'],
metadata: metadata!,
privateMetadata: privateMetadata,
publicMetadata: publicMetadata,
info: info,
);
}
List<Object?> remoteAssetToRow(RemoteAsset asset) {
return [
asset.id,
asset.ownerID,
asset.fileHeader,
asset.thumbHeader,
asset.creationTime,
asset.modificationTime,
asset.type,
asset.subType,
asset.title,
asset.fileSize,
asset.hash,
asset.visibility,
asset.durationInSec,
asset.location?.latitude,
asset.location?.longitude,
asset.height,
asset.width,
asset.noThumb,
asset.sv,
asset.mediaType,
asset.motionVideoIndex,
asset.caption,
asset.uploaderName,
];
}
RemoteAsset fromFilesRow(Map<String, Object?> row) {
return RemoteAsset(
id: row['id'] as int,
ownerID: row['owner_id'] as int,
thumbHeader: row['thumb_header'] as Uint8List,
fileHeader: row['file_header'] as Uint8List,
creationTime: row['creation_time'] as int,
modificationTime: row['modification_time'] as int,
type: row['type'] as int,
subType: row['subtype'] as int,
title: row['title'] as String,
fileSize: row['size'] as int?,
hash: row['hash'] as String?,
visibility: row['visibility'] as int?,
durationInSec: row['durationInSec'] as int?,
location: Location(
latitude: (row['lat'] as num?)?.toDouble(),
longitude: (row['lng'] as num?)?.toDouble(),
),
height: row['height'] as int?,
width: row['width'] as int?,
noThumb: row['no_thumb'] as int?,
sv: row['sv'] as int?,
mediaType: row['media_type'] as int?,
motionVideoIndex: row['motion_video_index'] as int?,
caption: row['caption'] as String?,
uploaderName: row['uploader_name'] as String?,
);
}
RLMapping rowToUploadLocalMapping(Map<String, Object?> row) {
return RLMapping(
remoteUploadID: row['file_id'] as int,
localID: row['local_id'] as String,
localCloudID: row['local_cloud_id'] as String?,
mappingType:
MappingTypeExtension.fromName(row['local_mapping_src'] as String),
);
}
EnteFile trashRowToEnteFile(Map<String, Object?> row) {
final RemoteAsset asset = fromTrashRow(row);
final TrashTime time = TrashTime(
createdAt: row['created_at'] as int,
updatedAt: row['updated_at'] as int,
deleteBy: row['delete_by'] as int,
);
final cf = CollectionFile(
fileID: asset.id,
collectionID: row['collection_id'] as int,
encFileKey: row['enc_key'] as Uint8List,
encFileKeyNonce: row['enc_key_nonce'] as Uint8List,
updatedAt: time.updatedAt,
createdAt: time.createdAt,
);
final file = EnteFile.fromRemoteAsset(asset, cf);
file.trashTime = time;
return file;
}

View File

@@ -0,0 +1,235 @@
const collectionColumns =
'id, owner, enc_key, enc_key_nonce, name, type, local_path, is_deleted, '
'updation_time, sharees, public_urls, mmd_encoded_json, '
'mmd_ver, pub_mmd_encoded_json, pub_mmd_ver, shared_mmd_json, '
'shared_mmd_ver';
final String updateCollectionColumns = collectionColumns
.split(', ')
.where((column) => column != 'id') // Exclude primary key from update
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const collectionFilesColumns =
'collection_id, file_id, enc_key, enc_key_nonce, created_at, updated_at';
final String collectionFilesUpdateColumns = collectionFilesColumns
.split(', ')
.where(
(column) =>
column != 'collection_id' ||
column != 'file_id' ||
column != 'created_at',
)
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const filesColumns =
'id, owner_id, file_header, thumb_header, creation_time, modification_time, '
'type, subtype, title, size, hash, visibility, durationInSec, lat, lng, '
'height, width, no_thumb, sv, media_type, motion_video_index, caption, uploader_name';
final String filesUpdateColumns = filesColumns
.split(', ')
.where((column) => (column != 'id'))
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const filesMetadataColumns = 'id, metadata, priv_metadata, pub_metadata, info';
final String filesMetadataUpdateColumns = filesMetadataColumns
.split(', ')
.where((column) => (column != 'id'))
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const trashedFilesColumns =
'id, owner_id, collection_id, enc_key,enc_key_nonce, file_header, thumb_header, metadata, priv_metadata, pub_metadata, info, created_at, updated_at, delete_by';
final String trashedFilesUpdateColumns = trashedFilesColumns
.split(', ')
.where((column) => (column != 'id'))
.map((column) => '$column = excluded.$column') // Use excluded virtual table
.join(', ');
const uploadLocalMappingColumns =
'file_id, local_id, local_cloud_id, local_mapping_src';
String collectionValuePlaceHolder =
collectionColumns.split(',').map((_) => '?').join(',');
class RemoteDBMigration {
static const migrationScripts = [
'''
CREATE TABLE collections (
id INTEGER PRIMARY KEY,
owner TEXT NOT NULL,
enc_key TEXT NOT NULL,
enc_key_nonce TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
local_path TEXT,
is_deleted INTEGER NOT NULL,
updation_time INTEGER NOT NULL,
sharees TEXT NOT NULL DEFAULT '[]',
public_urls TEXT NOT NULL DEFAULT '[]',
mmd_encoded_json TEXT NOT NULL DEFAULT '{}',
mmd_ver INTEGER NOT NULL DEFAULT 0,
pub_mmd_encoded_json TEXT DEFAULT '{}',
pub_mmd_ver INTEGER NOT NULL DEFAULT 0,
shared_mmd_json TEXT NOT NULL DEFAULT '{}',
shared_mmd_ver INTEGER NOT NULL DEFAULT 0
);
''',
'''
CREATE TABLE collection_files (
file_id INTEGER NOT NULL,
collection_id INTEGER NOT NULL,
enc_key BLOB NOT NULL,
enc_key_nonce BLOB NOT NULL,
updated_at INTEGER NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (file_id, collection_id)
);
''',
'''
CREATE TABLE files (
id INTEGER PRIMARY KEY,
owner_id INTEGER NOT NULL,
file_header BLOB NOT NULL,
thumb_header BLOB NOT NULL,
creation_time INTEGER NOT NULL,
modification_time INTEGER NOT NULL,
type INTEGER NOT NULL,
subtype INTEGER NOT NULL,
title TEXT NOT NULL,
size INTEGER,
hash TEXT,
visibility integer,
durationInSec INTEGER,
lat REAL DEFAULT NULL,
lng REAL DEFAULT NULL,
height INTEGER,
width INTEGER,
no_thumb INTEGER,
sv INTEGER,
media_type INTEGER,
motion_video_index INTEGER,
caption TEXT,
uploader_name TEXT
)
''',
'''
CREATE INDEX IF NOT EXISTS file_hash_index ON files(hash);
''',
'''
CREATE INDEX IF NOT EXISTS file_creation_time_index ON files(creation_time);
''',
'''
CREATE TABLE files_metadata (
id INTEGER PRIMARY KEY,
metadata TEXT NOT NULL,
priv_metadata TEXT,
pub_metadata TEXT,
info TEXT,
FOREIGN KEY (id) REFERENCES files(id) ON DELETE CASCADE
)
''',
'''
CREATE TABLE trash (
id INTEGER PRIMARY KEY,
owner_id INTEGER NOT NULL,
collection_id INTEGER NOT NULL,
enc_key BLOB NOT NULL,
enc_key_nonce BLOB NOT NULL,
metadata TEXT NOT NULL,
priv_metadata TEXT,
pub_metadata TEXT,
info TEXT,
file_header BLOB NOT NULL,
thumb_header BLOB NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_by INTEGER NOT NULL
)
''',
'''
CREATE TRIGGER delete_orphaned_files
AFTER DELETE ON collection_files
FOR EACH ROW
WHEN (
-- Only proceed if this file_id actually existed before deletion
OLD.file_id IS NOT NULL
-- And only if this was the last reference to the file
AND NOT EXISTS (
SELECT 1
FROM collection_files
WHERE file_id = OLD.file_id
)
)
BEGIN
-- Only then delete from files table
DELETE FROM files WHERE id = OLD.file_id;
END;
''',
'''
CREATE TABLE upload_mapping (
file_id INTEGER PRIMARY KEY,
local_id TEXT NOT NULL,
-- icloud identifier if available
local_cloud_id TEXT,
local_mapping_src TEXT DEFAULT NULL,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
)'''
];
}
class FilterQueryParam {
int? collectionID;
int? limit;
int? offset;
String? orderByColumn;
bool isAsc;
(int?, int?)? createAtRange;
FilterQueryParam({
this.limit,
this.offset,
this.collectionID,
this.orderByColumn = "creation_time",
this.isAsc = false,
this.createAtRange,
});
String get orderBy => orderByColumn == null
? ""
: "ORDER BY $orderByColumn ${isAsc ? "ASC" : "DESC"}";
String get limitOffset => (limit != null && offset != null)
? "LIMIT $limit + OFFSET $offset)"
: (limit != null)
? "LIMIT $limit"
: "";
String get collectionFilter =>
(collectionID == null) ? "" : "collection_id = $collectionID";
String get createAtRangeStr => (createAtRange == null ||
createAtRange!.$1 == null)
? ""
: "(creation_time BETWEEN ${createAtRange!.$1} AND ${createAtRange!.$2})";
String whereClause() {
final where = <String>[];
if (collectionFilter.isNotEmpty) {
where.add(collectionFilter);
}
if (createAtRangeStr.isNotEmpty) {
where.add(createAtRangeStr);
}
return (where.isEmpty ? "" : where.join(" AND ")) +
" " +
orderBy +
" " +
limitOffset;
}
}

View File

@@ -0,0 +1,288 @@
import "package:flutter/foundation.dart";
import "package:photos/db/remote/db.dart";
import "package:photos/db/remote/schema.dart";
import "package:photos/extensions/stop_watch.dart";
import "package:photos/models/file/remote/collection_file.dart";
extension CollectionFiles on RemoteDB {
Future<int> getCollectionFileCount(int collectionID) async {
final row = await sqliteDB.get(
"SELECT COUNT(*) as count FROM collection_files WHERE collection_id = ?",
[collectionID],
);
return row["count"] as int;
}
Future<Set<int>> getUploadedFileIDs(int collectionID) async {
final rows = await sqliteDB.getAll(
"SELECT file_id FROM collection_files WHERE collection_id = ?",
[collectionID],
);
final Set<int> fileIDs = {};
for (var row in rows) {
fileIDs.add(row["file_id"] as int);
}
return fileIDs;
}
Future<Set<int>> getAllCollectionIDsOfFile(int fileID) async {
final rows = await sqliteDB.getAll(
"SELECT collection_id FROM collection_files WHERE file_id = ?",
[fileID],
);
final Set<int> collectionIDs = {};
for (var row in rows) {
collectionIDs.add(row["collection_id"] as int);
}
return collectionIDs;
}
Future<Map<int, List<CollectionFile>>> getCollectionFilesGroupedByCollection(
List<int> fileIDs,
) async {
final result = <int, List<CollectionFile>>{};
if (fileIDs.isEmpty) {
return result;
}
final inParam = fileIDs.map((id) => "'$id'").join(',');
final results = await sqliteDB.getAll(
'SELECT * FROM collection_files WHERE file_id IN ($inParam)',
);
for (final row in results) {
final eachFile = CollectionFile.fromMap(row);
if (!result.containsKey(eachFile.collectionID)) {
result[eachFile.collectionID] = <CollectionFile>[];
}
result[eachFile.collectionID]!.add(eachFile);
}
return result;
}
Future<List<CollectionFile>> getAllCFForFileIDs(
List<int> fileIDs,
) async {
if (fileIDs.isEmpty) return [];
final rows = await sqliteDB.getAll(
"SELECT * FROM collection_files WHERE file_id IN (${fileIDs.join(",")})",
);
return rows
.map((row) => CollectionFile.fromMap(row))
.toList(growable: false);
}
Future<Map<int, int>> getCollectionIdToFileCount(List<int> fileIDs) async {
final rows = await sqliteDB.getAll(
"SELECT collection_id, COUNT(*) as count FROM collection_files WHERE file_id IN (${fileIDs.join(",")}) GROUP BY collection_id",
);
final Map<int, int> collectionIdToFileCount = {};
for (var row in rows) {
final collectionId = row["collection_id"] as int;
final count = row["count"] as int;
collectionIdToFileCount[collectionId] = count;
}
return collectionIdToFileCount;
}
Future<List<CollectionFile>> getCollectionFiles(
FilterQueryParam? params,
) async {
final rows = await sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files JOIN files on collection_files.file_id=files.id WHERE ${params?.whereClause() ?? "order by creation_time desc"}",
);
return rows
.map((row) => CollectionFile.fromMap(row))
.toList(growable: false);
}
Future<List<CollectionFile>> getCollectionsFiles(
Set<int> collectionIDs,
) async {
final rows = await sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files JOIN files on collection_files.file_id=files.id WHERE collection_id IN (${collectionIDs.join(",")}) ORDER BY creation_time DESC",
);
return rows
.map((row) => CollectionFile.fromMap(row))
.toList(growable: false);
}
Future<Map<int, CollectionFile>> getFileIdToCollectionFile(
List<int> fileIDs,
) async {
final rows = await sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files JOIN files on collection_files.file_id=files.id WHERE file_id IN (${fileIDs.join(",")})",
);
final Map<int, CollectionFile> result = {};
for (var row in rows) {
final entry = CollectionFile.fromMap(row);
result[entry.fileID] = entry;
}
return result;
}
Future<List<CollectionFile>> getAllFiles(int userID) {
return sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files JOIN files ON collection_files.file_id = files.id WHERE files.owner_id = ? ORDER BY files.creation_time DESC",
[userID],
).then(
(rows) => rows.map((row) => CollectionFile.fromMap(row)).toList(),
);
}
Future<(Set<int>, Map<String, int>)> getUploadAndHash(
int collectionID,
) async {
final results = await sqliteDB.getAll(
'SELECT id, hash FROM collection_files JOIN files ON files.id = collection_files.file_id'
' WHERE collection_id = ?',
[
collectionID,
],
);
final ids = <int>{};
final hash = <String, int>{};
for (final result in results) {
ids.add(result['id'] as int);
if (result['hash'] != null) {
hash[result['hash'] as String] = result['id'] as int;
}
}
return (ids, hash);
}
Future<List<CollectionFile>> ownedFilesWithSameHash(
List<String> hashes,
int ownerID,
) async {
if (hashes.isEmpty) return [];
final inParam = hashes.map((e) => "'$e'").join(',');
final rows = await sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files JOIN files ON collection_files.file_id = files.id WHERE files.hash IN ($inParam) AND files.owner_id = ?",
[ownerID],
);
return rows
.map((row) => CollectionFile.fromMap(row))
.toList(growable: false);
}
Future<CollectionFile?> coverFile(
int collectionID,
int? fileID, {
bool sortInAsc = false,
}) async {
if (fileID != null) {
final entry = await getCollectionFileEntry(collectionID, fileID);
if (entry != null) {
return entry;
}
}
final sortedRow = await sqliteDB.getOptional(
"SELECT collection_files.* FROM collection_files join files on files.id= collection_files.file_id WHERE collection_id = ? ORDER BY files.creation_time ${sortInAsc ? 'ASC' : 'DESC'} LIMIT 1",
[collectionID],
);
if (sortedRow != null) {
return CollectionFile.fromMap(sortedRow);
}
return null;
}
Future<CollectionFile?> getCollectionFileEntry(
int collectionID,
int fileID,
) async {
final row = await sqliteDB.getOptional(
"SELECT * FROM collection_files WHERE collection_id = ? AND file_id = ?",
[collectionID, fileID],
);
if (row != null) {
return CollectionFile.fromMap(row);
}
return null;
}
Future<CollectionFile?> getAnyCollectionEntry(
int fileID,
) async {
final row = await sqliteDB.getAll(
"SELECT * FROM collection_files WHERE file_id = ? limit 1",
[fileID],
);
if (row.isNotEmpty) {
return CollectionFile.fromMap(row.first);
}
return null;
}
Future<List<CollectionFile>> getFilesCreatedWithinDurations(
List<List<int>> durations,
Set<int> ignoredCollectionIDs, {
String order = 'DESC',
}) async {
final List<CollectionFile> result = [];
for (final duration in durations) {
final start = duration[0];
final end = duration[1];
final rows = await sqliteDB.getAll(
"SELECT collection_files.* FROM collection_files join files on files.id=collection_files.file_id WHERE files.creation_time BETWEEN ? AND ? AND collection_id NOT IN (${ignoredCollectionIDs.join(",")}) ORDER BY creation_time $order",
[start, end],
);
result.addAll(rows.map((row) => CollectionFile.fromMap(row)));
}
return result;
}
Future<List<CollectionFile>> filesWithLocation() {
return sqliteDB
.getAll(
"SELECT collection_files.* FROM collection_files JOIN files ON collection_files.file_id = files.id WHERE files.lat IS NOT NULL and files.lng IS NOT NULL order by files.creation_time desc",
)
.then(
(rows) => rows.map((row) => CollectionFile.fromMap(row)).toList(),
);
}
Future<void> deleteFiles(List<int> fileIDs) async {
if (fileIDs.isEmpty) return;
final stopwatch = Stopwatch()..start();
await sqliteDB.execute(
"DELETE FROM collection_files WHERE file_id IN (${fileIDs.join(",")})",
);
debugPrint(
'$runtimeType deleteFiles complete in ${stopwatch.elapsed.inMilliseconds}ms for ${fileIDs.length}',
);
}
Future<void> deleteCollectionFiles(List<int> cIDs) async {
if (cIDs.isEmpty) return;
await sqliteDB.execute(
"DELETE FROM collection_files WHERE collection_id IN (${cIDs.join(",")})",
);
}
Future<void> deleteCFEnteries(
int collectionID,
List<int> fileIDs,
) async {
if (fileIDs.isEmpty) return;
await sqliteDB.execute(
"DELETE FROM collection_files WHERE collection_id = ? AND file_id IN (${fileIDs.join(",")})",
[collectionID],
);
}
Future<Map<int, int>> getCollectionIDToMaxCreationTime() async {
final enteWatch = EnteWatch("getCollectionIDToMaxCreationTime")..start();
final rows = await sqliteDB.getAll(
'''SELECT collection_id, MAX(creation_time) as max_creation_time FROM collection_files join files on
collection_files.file_id=files.id GROUP BY collection_id''',
);
final Map<int, int> result = {};
for (var row in rows) {
final collectionId = row["collection_id"] as int;
final maxCreationTime = row["max_creation_time"] as int;
result[collectionId] = maxCreationTime;
}
enteWatch.log("query done");
return result;
}
}

View File

@@ -0,0 +1,156 @@
import "package:photos/db/remote/db.dart";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/file/file_type.dart";
extension FilesTable on RemoteDB {
// For a given userID, return unique uploadedFileId for the given userID
Future<List<int>> fileIDsWithMissingSize(int userId) async {
final rows = await sqliteDB.getAll(
"SELECT id FROM files WHERE owner_id = ? AND size = -1",
[userId],
);
final result = <int>[];
for (final row in rows) {
result.add(row['id'] as int);
}
return result;
}
Future<Map<int, int>> getIDToCreationTime() async {
final rows = await sqliteDB.getAll(
"SELECT id, creation_time FROM files",
);
final result = <int, int>{};
for (final row in rows) {
result[row['id'] as int] = row['creation_time'] as int;
}
return result;
}
Future<Map<int, Metadata?>> getIDToMetadata(
Set<int> ids, {
bool private = false,
bool public = false,
bool metadata = false,
}) async {
if (ids.isEmpty) return {};
// Ensure only one parameter is true
final trueCount = [private, public, metadata].where((x) => x).length;
if (trueCount != 1) {
throw ArgumentError(
'Exactly one of private, public, or metadata must be true',
);
}
final placeholders = List.filled(ids.length, '?').join(',');
String column;
if (private) {
column = 'priv_metadata';
} else if (public) {
column = 'pub_metadata';
} else {
column = 'metadata';
}
final rows = await sqliteDB.getAll(
"SELECT id, $column FROM files_metadata WHERE id IN ($placeholders)",
ids.toList(),
);
final result = <int, Metadata?>{};
for (final row in rows) {
final metadata = Metadata.fromEncodedJson(row[column]);
result[row['id'] as int] = metadata;
}
return result;
}
Future<Set<int>> idsWithSameHashAndType(String hash, int ownerID) {
return sqliteDB.getAll(
"SELECT id FROM files WHERE hash = ? AND owner_id = ?",
[hash, ownerID],
).then((rows) {
final result = <int>{};
for (final row in rows) {
result.add(row['id'] as int);
}
return result;
});
}
// updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and
// update the fileSize for the given uploadedFileID
Future<void> updateSize(
Map<int, int> idToSize,
) async {
final parameterSets = <List<Object?>>[];
for (final id in idToSize.keys) {
parameterSets.add([idToSize[id], id]);
}
return sqliteDB.executeBatch(
"UPDATE files SET size = ? WHERE id = ?;",
parameterSets,
);
}
Future<List<int>> getAllFilesAfterDate({
required FileType fileType,
required DateTime beginDate,
required int userID,
}) async {
final results = await sqliteDB.getAll(
'''
SELECT files.id FROM files join upload_mapping
ON files.id = upload_mapping.file_id
WHERE file_type = ?
AND creation_time > ?
AND owner_id = ?
AND (size IS NOT NULL AND size <= 524288000)
AND (durationInSec IS NOT NULL AND (durationInSec <= 60 AND durationInSec > 0))
''',
[getInt(fileType), beginDate.microsecondsSinceEpoch, userID],
);
final fileIDs = <int>[];
for (final row in results) {
fileIDs.add(row['id'] as int);
}
return fileIDs;
}
Future<Map<int, List<(int, Metadata?)>>> getNotificationCandidate(
List<int> collectionIDs,
int lastAppOpen,
) async {
if (collectionIDs.isEmpty) return {};
final placeholders = List.filled(collectionIDs.length, '?').join(',');
final rows = await sqliteDB.getAll(
"SELECT collection_id, files.owner_id, metadata FROM collection_files join files ON collection_files.file_id = files.id WHERE collection_id IN ($placeholders) AND collection_files.created_at > ?",
[...collectionIDs, lastAppOpen],
);
final result = <int, List<(int, Metadata?)>>{};
for (final row in rows) {
final collectionID = row['collection_id'] as int;
final ownerID = row['owner_id'] as int;
final metadata = Metadata.fromEncodedJson(row['metadata']);
result.putIfAbsent(collectionID, () => []).add((ownerID, metadata));
}
return result;
}
Future<int> getFilesCountByVisibility(
int visibility,
int ownerID,
Set<int> hiddenCollections,
) async {
String subQuery = '';
if (hiddenCollections.isNotEmpty) {
subQuery =
'AND id NOT IN (SELECT file_id FROM collection_files WHERE collection_id IN (${hiddenCollections.join(',')}))';
}
final row = await sqliteDB.get(
'SELECT COUNT(id) as count FROM files WHERE visibility = ? AND owner_id = ? $subQuery',
[visibility, ownerID],
);
return row['count'] as int;
}
}

View File

@@ -0,0 +1,119 @@
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:photos/db/remote/db.dart";
import "package:photos/db/remote/mappers.dart";
import "package:photos/db/remote/schema.dart";
import "package:photos/models/backup_status.dart";
import "package:photos/models/file/remote/rl_mapping.dart";
extension UploadMappingTable on RemoteDB {
Future<void> insertMappings(List<RLMapping> mappings) async {
if (mappings.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(mappings.slices(1000), (slice) async {
final List<List<Object?>> values = slice.map((e) => e.rowValues).toList();
await sqliteDB.executeBatch(
'INSERT INTO upload_mapping ($uploadLocalMappingColumns) values(?,?,?,?)',
values,
);
});
debugPrint(
'$runtimeType insertMappings complete in ${stopwatch.elapsed.inMilliseconds}ms for ${mappings.length} mappings',
);
}
Future<List<RLMapping>> getMappings() async {
final result = <RLMapping>[];
final cursor = await sqliteDB.getAll("SELECT * FROM upload_mapping");
for (final row in cursor) {
result.add(rowToUploadLocalMapping(row));
}
return result;
}
Future<void> deleteMappingsForLocalIDs(Set<String> localIDs) async {
if (localIDs.isEmpty) return;
final placeholders = List.filled(localIDs.length, '?').join(',');
await sqliteDB.execute(
'DELETE FROM upload_mapping WHERE local_id IN ($placeholders)',
localIDs.toList(),
);
}
Future<Map<String, RLMapping>> getLocalIDToMappingForActiveFiles() async {
final result = <String, RLMapping>{};
final cursor = await sqliteDB.getAll(
"SELECT * FROM upload_mapping join files on upload_mapping.file_id = files.id",
);
for (final row in cursor) {
final mapping = rowToUploadLocalMapping(row);
result[mapping.localID] = mapping;
}
return result;
}
// getLocalIDsForUser returns information about the localIDs that have been
// uploaded for the given userID. If the localIDSInGivenPath is not null,
// it will only return the localIDs that are in the given path.
Future<BackedUpFileIDs> getLocalIDsForUser(
int userID,
Set<String>? localIDSInGivenPath,
) async {
final results = await sqliteDB.getAll(
'SELECT local_id, files.id, size FROM upload_mapping join files on upload_mapping.file_id = files.id WHERE owner_id = ?',
[userID],
);
final Set<String> localIDs = <String>{};
final Set<int> uploadedIDs = <int>{};
int localSize = 0;
for (final result in results) {
final String localID = result['local_id'] as String;
if (localIDSInGivenPath != null &&
!localIDSInGivenPath.contains(localID)) {
continue; // Skip if not in the given path
}
final int? fileSize = result['size'] as int?;
if (!localIDs.contains(localID) && fileSize != null) {
localSize += fileSize;
}
localIDs.add(localID);
uploadedIDs.add(result['id'] as int);
}
return BackedUpFileIDs(localIDs.toList(), uploadedIDs.toList(), localSize);
}
Future<Set<String>> getLocalIDsWithMapping(List<String> localIDs) async {
if (localIDs.isEmpty) return {};
final placeholders = List.filled(localIDs.length, '?').join(',');
final cursor = await sqliteDB.getAll(
'SELECT local_id FROM upload_mapping join files on upload_mapping.file_id = files.id WHERE local_id IN ($placeholders)',
localIDs,
);
return cursor.map((row) => row['local_id'] as String).toSet();
}
Future<Map<int, String>> getFileIDToLocalIDMapping(List<int> fileIDs) async {
if (fileIDs.isEmpty) return {};
final placeholders = List.filled(fileIDs.length, '?').join(',');
final cursor = await sqliteDB.getAll(
'SELECT file_id, local_id FROM upload_mapping WHERE file_id IN ($placeholders)',
fileIDs,
);
return Map.fromEntries(
cursor.map(
(row) => MapEntry(row['file_id'] as int, row['local_id'] as String),
),
);
}
Future<Set<int>> getFilesWithMapping(List<int> fileIDs) async {
if (fileIDs.isEmpty) return {};
final placeholders = List.filled(fileIDs.length, '?').join(',');
final cursor = await sqliteDB.getAll(
'SELECT file_id FROM upload_mapping WHERE file_id IN ($placeholders)',
fileIDs,
);
return cursor.map((row) => row['file_id'] as int).toSet();
}
}

View File

@@ -0,0 +1,49 @@
import "package:collection/collection.dart";
import "package:flutter/foundation.dart";
import "package:photos/db/remote/db.dart";
import "package:photos/db/remote/mappers.dart";
import "package:photos/db/remote/schema.dart";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/file/file.dart";
extension TrashTable on RemoteDB {
Future<void> insertTrashDiffItems(List<DiffItem> items) async {
if (items.isEmpty) return;
final stopwatch = Stopwatch()..start();
await Future.forEach(items.slices(1000), (slice) async {
final List<List<Object?>> trashRowValues = [];
for (final item in slice) {
trashRowValues.add(item.trashRowValues());
}
await Future.wait([
sqliteDB.executeBatch(
'INSERT INTO trash ($trashedFilesColumns) values(${getParams(14)})',
trashRowValues,
),
]);
});
debugPrint(
'$runtimeType insertCollectionFilesDiff complete in ${stopwatch.elapsed.inMilliseconds}ms for ${items.length}',
);
}
// removes the items and returns the number of items removed
Future<int> removeTrashItems(List<int> ids) async {
if (ids.isEmpty) return 0;
final result = await sqliteDB.execute(
'DELETE FROM trash WHERE id IN (${ids.join(",")})',
);
return result.isNotEmpty ? result.first['changes'] as int : 0;
}
Future<List<EnteFile>> getTrashFiles() async {
final result = await sqliteDB.getAll(
'SELECT * FROM trash',
);
return result.map((e) => trashRowToEnteFile(e)).toList();
}
Future<void> clearTrash() async {
await sqliteDB.execute('DELETE FROM trash');
}
}

View File

@@ -1,250 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/models/file/trash_file.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:sqflite/sqflite.dart';
// The TrashDB doesn't need to flatten and store all attributes of a file.
// Before adding any other column, we should evaluate if we need to query on that
// column or not while showing trashed items. Even if we miss storing any new attributes,
// during restore, all file attributes will be fetched & stored as required.
class TrashDB {
static const _databaseName = "ente.trash.db";
static const _databaseVersion = 1;
static final Logger _logger = Logger("TrashDB");
static const tableName = 'trash';
static const columnUploadedFileID = 'uploaded_file_id';
static const columnCollectionID = 'collection_id';
static const columnOwnerID = 'owner_id';
static const columnTrashUpdatedAt = 't_updated_at';
static const columnTrashDeleteBy = 't_delete_by';
static const columnEncryptedKey = 'encrypted_key';
static const columnKeyDecryptionNonce = 'key_decryption_nonce';
static const columnFileDecryptionHeader = 'file_decryption_header';
static const columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
static const columnUpdationTime = 'updation_time';
static const columnCreationTime = 'creation_time';
static const columnLocalID = 'local_id';
// standard file metadata, which isn't editable
static const columnFileMetadata = 'file_metadata';
static const columnMMdEncodedJson = 'mmd_encoded_json';
static const columnMMdVersion = 'mmd_ver';
static const columnPubMMdEncodedJson = 'pub_mmd_encoded_json';
static const columnPubMMdVersion = 'pub_mmd_ver';
Future _onCreate(Database db, int version) async {
await db.execute(
'''
CREATE TABLE $tableName (
$columnUploadedFileID INTEGER PRIMARY KEY NOT NULL,
$columnCollectionID INTEGER NOT NULL,
$columnOwnerID INTEGER,
$columnTrashUpdatedAt INTEGER NOT NULL,
$columnTrashDeleteBy INTEGER NOT NULL,
$columnEncryptedKey TEXT,
$columnKeyDecryptionNonce TEXT,
$columnFileDecryptionHeader TEXT,
$columnThumbnailDecryptionHeader TEXT,
$columnUpdationTime INTEGER,
$columnLocalID TEXT,
$columnCreationTime INTEGER NOT NULL,
$columnFileMetadata TEXT DEFAULT '{}',
$columnMMdEncodedJson TEXT DEFAULT '{}',
$columnMMdVersion INTEGER DEFAULT 0,
$columnPubMMdEncodedJson TEXT DEFAULT '{}',
$columnPubMMdVersion INTEGER DEFAULT 0
);
CREATE INDEX IF NOT EXISTS creation_time_index ON $tableName($columnCreationTime);
CREATE INDEX IF NOT EXISTS delete_by_time_index ON $tableName($columnTrashDeleteBy);
CREATE INDEX IF NOT EXISTS updated_at_time_index ON $tableName($columnTrashUpdatedAt);
''',
);
}
TrashDB._privateConstructor();
static final TrashDB instance = TrashDB._privateConstructor();
// only have a single app-wide reference to the database
static Future<Database>? _dbFuture;
Future<Database> get database async {
// lazily instantiate the db the first time it is accessed
_dbFuture ??= _initDatabase();
return _dbFuture!;
}
// this opens the database (and creates it if it doesn't exist)
Future<Database> _initDatabase() async {
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
_logger.info("DB path " + path);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
);
}
Future<void> clearTable() async {
final db = await instance.database;
await db.delete(tableName);
}
Future<int> count() async {
final db = await instance.database;
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM $tableName'),
);
return count ?? 0;
}
Future<void> insertMultiple(List<TrashFile> trashFiles) async {
final startTime = DateTime.now();
final db = await instance.database;
var batch = db.batch();
int batchCounter = 0;
for (TrashFile trash in trashFiles) {
if (batchCounter == 400) {
await batch.commit(noResult: true);
batch = db.batch();
batchCounter = 0;
}
batch.insert(
tableName,
_getRowForTrash(trash),
conflictAlgorithm: ConflictAlgorithm.replace,
);
batchCounter++;
}
await batch.commit(noResult: true);
final endTime = DateTime.now();
final duration = Duration(
microseconds:
endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch,
);
_logger.info(
"Batch insert of " +
trashFiles.length.toString() +
" took " +
duration.inMilliseconds.toString() +
"ms.",
);
}
Future<int> delete(List<int> uploadedFileIDs) async {
final db = await instance.database;
return db.delete(
tableName,
where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})',
);
}
Future<int> update(TrashFile file) async {
final db = await instance.database;
return await db.update(
tableName,
_getRowForTrash(file),
where: '$columnUploadedFileID = ?',
whereArgs: [file.uploadedFileID],
);
}
Future<FileLoadResult> getTrashedFiles(
int startTime,
int endTime, {
int? limit,
bool? asc,
}) async {
final db = await instance.database;
final order = (asc ?? false ? 'ASC' : 'DESC');
final results = await db.query(
tableName,
where: '$columnCreationTime >= ? AND $columnCreationTime <= ?',
whereArgs: [startTime, endTime],
orderBy: '$columnCreationTime ' + order,
limit: limit,
);
final files = _convertToFiles(results);
return FileLoadResult(files, files.length == limit);
}
List<TrashFile> _convertToFiles(List<Map<String, dynamic>> results) {
final List<TrashFile> trashedFiles = [];
for (final result in results) {
trashedFiles.add(_getTrashFromRow(result));
}
return trashedFiles;
}
TrashFile _getTrashFromRow(Map<String, dynamic> row) {
final trashFile = TrashFile();
trashFile.updateAt = row[columnTrashUpdatedAt];
trashFile.deleteBy = row[columnTrashDeleteBy];
trashFile.uploadedFileID = row[columnUploadedFileID];
// dirty hack to ensure that the file_downloads & cache mechanism works
trashFile.generatedID = -1 * trashFile.uploadedFileID!;
trashFile.ownerID = row[columnOwnerID];
trashFile.collectionID =
row[columnCollectionID] == -1 ? null : row[columnCollectionID];
trashFile.encryptedKey = row[columnEncryptedKey];
trashFile.keyDecryptionNonce = row[columnKeyDecryptionNonce];
trashFile.fileDecryptionHeader = row[columnFileDecryptionHeader];
trashFile.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
trashFile.updationTime = row[columnUpdationTime] ?? 0;
trashFile.creationTime = row[columnCreationTime];
final fileMetadata = row[columnFileMetadata] ?? '{}';
trashFile.applyMetadata(jsonDecode(fileMetadata));
trashFile.localID = row[columnLocalID];
trashFile.mMdVersion = row[columnMMdVersion] ?? 0;
trashFile.mMdEncodedJson = row[columnMMdEncodedJson] ?? '{}';
trashFile.pubMmdVersion = row[columnPubMMdVersion] ?? 0;
trashFile.pubMmdEncodedJson = row[columnPubMMdEncodedJson] ?? '{}';
if (trashFile.pubMagicMetadata != null &&
trashFile.pubMagicMetadata!.editedTime != null) {
// override existing creationTime to avoid re-writing all queries related
// to loading the gallery
row[columnCreationTime] = trashFile.pubMagicMetadata!.editedTime!;
}
return trashFile;
}
Map<String, dynamic> _getRowForTrash(TrashFile trash) {
final row = <String, dynamic>{};
row[columnTrashUpdatedAt] = trash.updateAt;
row[columnTrashDeleteBy] = trash.deleteBy;
row[columnUploadedFileID] = trash.uploadedFileID;
row[columnCollectionID] = trash.collectionID;
row[columnOwnerID] = trash.ownerID;
row[columnEncryptedKey] = trash.encryptedKey;
row[columnKeyDecryptionNonce] = trash.keyDecryptionNonce;
row[columnFileDecryptionHeader] = trash.fileDecryptionHeader;
row[columnThumbnailDecryptionHeader] = trash.thumbnailDecryptionHeader;
row[columnUpdationTime] = trash.updationTime;
row[columnLocalID] = trash.localID;
row[columnCreationTime] = trash.creationTime;
row[columnFileMetadata] = jsonEncode(trash.metadata);
row[columnMMdVersion] = trash.mMdVersion;
row[columnMMdEncodedJson] = trash.mMdEncodedJson ?? '{}';
row[columnPubMMdVersion] = trash.pubMmdVersion;
row[columnPubMMdEncodedJson] = trash.pubMmdEncodedJson ?? '{}';
return row;
}
}

View File

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

View File

@@ -0,0 +1,10 @@
import "package:photos/events/event.dart";
class LocalAssetChangedEvent extends Event {
final String source;
LocalAssetChangedEvent(this.source);
@override
String get reason => '$runtimeType{"via": $source}';
}

View File

@@ -39,20 +39,26 @@ class EnteWatch extends Stopwatch {
class TimeLogger {
final String context;
final int logThreshold;
DateTime _start;
TimeLogger({this.context = "TLog", this.logThreshold = 5})
final DateTime _start;
DateTime _toStringStart = DateTime.now();
TimeLogger({this.context = "TLog:", this.logThreshold = 5})
: _start = DateTime.now();
@override
String toString() {
final int diff = DateTime.now().difference(_start).inMilliseconds;
final int diff = DateTime.now().difference(_toStringStart).inMilliseconds;
late String res;
if (diff > logThreshold) {
res = "[$context: $diff ms]";
res = "[$context$diff ms]";
} else {
res = "[]";
}
_start = DateTime.now();
_toStringStart = DateTime.now();
return res;
}
String get elapsed {
final int diff = DateTime.now().difference(_start).inMilliseconds;
return "[$context$diff ms]";
}
}

View File

@@ -0,0 +1,90 @@
import "package:flutter/foundation.dart";
import "package:photos/core/cache/lru_map.dart";
import "package:photos/models/file/file.dart";
// Singleton instance for global access
final enteImageCache = InMemoryImageCache._instance;
class InMemoryImageCache {
static final InMemoryImageCache _instance = InMemoryImageCache._();
// Private constructor for singleton
InMemoryImageCache._();
// Supported dimensions with associated cache sizes
static const Map<int, int> _cacheSizes = {
32: 5000, // Small: 32*32 = 1024 bytes * 5000 = 6.25MB
256: 2000, // Medium: 256*256 = 65536 bytes * 2000 = 128MB
512: 100, // Large: 512*512 = 262144 bytes * 100 = 25MB
};
// Cache instances for each dimension
final Map<int, LRUMap<String, Uint8List?>> _caches = {
32: LRUMap<String, Uint8List?>(5000),
256: LRUMap<String, Uint8List?>(2000),
512: LRUMap<String, Uint8List?>(100),
};
/// Gets a thumbnail for a file at the specified dimension
Uint8List? getThumb(EnteFile file, int dimension) {
return _getFromCache(file.cacheKey(), dimension);
}
/// Gets a thumbnail by ID at the specified dimension
Uint8List? getThumbByID(String id, int dimension) {
return _getFromCache(id, dimension);
}
/// Stores a thumbnail for a file at the specified dimension
void putThumb(EnteFile file, Uint8List? imageData, int dimension) {
_putInCache(file.cacheKey(), imageData, dimension);
}
/// Stores a thumbnail by ID at the specified dimension
void putThumbByID(String id, Uint8List? imageData, int dimension) {
_putInCache(id, imageData, dimension);
}
/// Checks if a thumbnail exists for a file at the specified dimension
bool containsThumb(EnteFile file, int dimension) {
return _isCached(file.cacheKey(), dimension);
}
void clearCache(EnteFile file) {
_caches.forEach((_, cache) {
cache.remove(file.cacheKey());
});
}
// Private helper methods
Uint8List? _getFromCache(String key, int dimension) {
if (_isValidDimension(dimension)) {
return _caches[dimension]?.get(key);
}
return null;
}
void _putInCache(String key, Uint8List? imageData, int dimension) {
if (_isValidDimension(dimension)) {
_caches[dimension]?.put(key, imageData);
} else {
debugPrint("Unsupported dimension: $dimension");
}
}
bool _isCached(String key, int dimension) {
if (_isValidDimension(dimension)) {
return _caches[dimension]?.containsKey(key) ?? false;
}
return false;
}
bool _isValidDimension(int dimension) {
if (_caches.containsKey(dimension)) {
return true;
}
debugPrint("Invalid dimension: $dimension");
return false;
}
}

View File

@@ -0,0 +1,193 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:cached_network_image/cached_network_image.dart';
import "package:equatable/equatable.dart";
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:photo_manager/photo_manager.dart';
import "package:photos/image/in_memory_image_cache.dart";
import "package:photos/utils/standalone/task_queue.dart";
final thumbnailQueue = TaskQueue<String>(
maxConcurrentTasks: 15,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 200, // Limit the queue to 50 pending tasks
);
final mediumThumbnailQueue = TaskQueue<String>(
maxConcurrentTasks: 5,
taskTimeout: const Duration(minutes: 1),
maxQueueSize: 200, // Limit the queue to 50 pending tasks
);
class LocalThumbnailProvider extends ImageProvider<LocalThumbnailProviderKey> {
final LocalThumbnailProviderKey key;
final int maxRetries;
final Duration retryDelay;
LocalThumbnailProvider(
this.key, {
this.maxRetries = 300,
this.retryDelay = const Duration(milliseconds: 5),
});
@override
Future<LocalThumbnailProviderKey> obtainKey(
ImageConfiguration configuration,
) async {
return SynchronousFuture<LocalThumbnailProviderKey>(key);
}
static cancelRequest(LocalThumbnailProviderKey key) {
thumbnailQueue.removeTask('${key.asset.id}-small');
mediumThumbnailQueue.removeTask('${key.asset.id}-medium');
}
@override
ImageStreamCompleter loadImage(
LocalThumbnailProviderKey key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiImageStreamCompleter(
codec: _codec(key, decode, chunkEvents),
scale: 1.0,
chunkEvents: chunkEvents.stream,
informationCollector: () sync* {
yield ErrorDescription('id: ${key.asset.id} name: ${key.asset.title}');
},
);
}
Stream<ui.Codec> _codec(
LocalThumbnailProviderKey key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async* {
// First try to get from cache
Uint8List? normalThumbBytes =
enteImageCache.getThumbByID(key.asset.id, key.height);
if (normalThumbBytes != null) {
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
final codec = await decode(buffer);
yield codec;
chunkEvents.close().ignore();
return;
}
// Try to load small thumbnail with retry logic
final Uint8List? thumbBytes = await _loadWithRetry(
key: key,
size: ThumbnailSize(key.smallThumbWidth, key.smallThumbHeight),
quality: 75,
cacheKey: '${key.asset.id}-small',
queue: thumbnailQueue,
cacheWidth: key.smallThumbWidth,
);
if (thumbBytes != null) {
final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes);
final codec = await decode(buffer);
yield codec;
} else {
debugPrint("$runtimeType smallThumb ${key.asset.title} failed");
}
// Try to load normal thumbnail with retry logic if not already in cache
if (normalThumbBytes == null) {
normalThumbBytes = await _loadWithRetry(
key: key,
size: ThumbnailSize(key.width, key.height),
quality: 50,
cacheKey: '${key.asset.id}-medium',
queue: mediumThumbnailQueue,
cacheWidth: key.height,
);
if (normalThumbBytes == null) {
throw StateError("$runtimeType biThumb ${key.asset.title} failed");
}
final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes);
final codec = await decode(buffer);
yield codec;
}
chunkEvents.close().ignore();
}
Future<Uint8List?> _loadWithRetry({
required LocalThumbnailProviderKey key,
required ThumbnailSize size,
required int quality,
required String cacheKey,
required TaskQueue<String> queue,
required int cacheWidth,
}) async {
int attempt = 0;
Uint8List? result;
while (attempt <= maxRetries) {
try {
// Check cache first on retry attempts
if (attempt > 0) {
result = enteImageCache.getThumbByID(key.asset.id, cacheWidth);
if (result != null) return result;
}
final Completer<Uint8List?> future = Completer();
await queue.addTask(cacheKey, () async {
final bytes =
await key.asset.thumbnailDataWithSize(size, quality: quality);
enteImageCache.putThumbByID(key.asset.id, bytes, cacheWidth);
future.complete(bytes);
});
result = await future.future;
return result;
} catch (e) {
// Only retry on specific exceptions
if (e is! TaskQueueOverflowException &&
e is! TaskQueueTimeoutException &&
e is! TaskQueueCancelledException) {
rethrow;
}
attempt++;
if (attempt <= maxRetries) {
await Future.delayed(retryDelay * attempt); // Exponential backoff
} else {
rethrow;
}
}
}
return null;
}
}
@immutable
class LocalThumbnailProviderKey extends Equatable {
final AssetEntity asset;
final int height;
final int width;
final int smallThumbHeight;
final int smallThumbWidth;
@override
List<Object?> get props => [
asset.id,
asset.modifiedDateSecond ?? 0,
height,
width,
smallThumbHeight,
smallThumbWidth,
];
const LocalThumbnailProviderKey({
required this.asset,
this.height = 256,
this.width = 256,
this.smallThumbWidth = 32,
this.smallThumbHeight = 32,
});
}

View File

@@ -372,7 +372,7 @@
"deleteFromBoth": "الحذف من كليهما",
"newAlbum": "ألبوم جديد",
"albums": "الألبومات",
"memoryCount": "{count, plural, =0 {لا توجد ذكريات} one {ذكرى واحدة} two {ذكريتان} few {{formattedCount} ذكريات} many {{formattedCount} ذكرى} other {{formattedCount} ذكرى}}",
"memoryCount": "{count, plural, =0 {لا توجد ذكريات} one {ذكرى واحدة} two {ذكريتان} other {{formattedCount} ذكرى}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -460,7 +460,7 @@
"skip": "تخط",
"updatingFolderSelection": "جارٍ تحديث تحديد المجلد...",
"itemCount": "{count, plural, one {{count} عُنْصُر} other {{count} عَنَاصِر}}",
"deleteItemCount": "{count, plural, =1 {حذف عنصر واحد} two {حذف عنصرين} few {حذف {count} عناصر} many {حذف {count} عنصرًا} other {حذف {count} عنصرًا}}",
"deleteItemCount": "{count, plural, =1 {حذف عنصر واحد} two {حذف عنصرين} other {حذف {count} عنصرًا}}",
"duplicateItemsGroup": "{count} ملفات، {formattedSize} لكل منها",
"@duplicateItemsGroup": {
"description": "Display the number of duplicate files and their size",
@@ -477,7 +477,7 @@
}
},
"showMemories": "عرض الذكريات",
"yearsAgo": "{count, plural, one {قبل سنة} two {قبل سنتين} few {قبل {count} سنوات} many {قبل {count} سنة} other {قبل {count} سنة}}",
"yearsAgo": "{count, plural, one {قبل سنة} two {قبل سنتين} other {قبل {count} سنة}}",
"backupSettings": "إعدادات النسخ الاحتياطي",
"backupStatus": "حالة النسخ الاحتياطي",
"backupStatusDescription": "ستظهر العناصر التي تم نسخها احتياطيًا هنا",
@@ -543,7 +543,7 @@
},
"remindToEmptyEnteTrash": "تذكر أيضًا إفراغ \"سلة المهملات\" لاستعادة المساحة المحررة.",
"sparkleSuccess": "✨ نجاح",
"duplicateFileCountWithStorageSaved": "لقد قمت بتنظيف {count, plural, one {ملف مكرر واحد} two {ملفين مكررين} few {{count} ملفات مكررة} many {{count} ملفًا مكررًا} other {{count} ملفًا مكررًا}}، مما وفر {storageSaved}!",
"duplicateFileCountWithStorageSaved": "لقد قمت بتنظيف {count, plural, one {ملف مكرر واحد} two {ملفين مكررين} other {{count} ملفًا مكررًا}}، مما وفر {storageSaved}!",
"@duplicateFileCountWithStorageSaved": {
"description": "The text to display when the user has successfully cleaned up duplicate files",
"type": "text",
@@ -794,11 +794,11 @@
"share": "مشاركة",
"unhideToAlbum": "إظهار في الألبوم",
"restoreToAlbum": "استعادة إلى الألبوم",
"moveItem": "{count, plural, =1 {نقل عنصر} two {نقل عنصرين} few {نقل {count} عناصر} many {نقل {count} عنصرًا} other {نقل {count} عنصرًا}}",
"moveItem": "{count, plural, =1 {نقل عنصر} two {نقل عنصرين} other {نقل {count} عنصرًا}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, =1 {إضافة عنصر} two {إضافة عنصرين} few {إضافة {count} عناصر} many {إضافة {count} عنصرًا} other {إضافة {count} عنصرًا}}",
"addItem": "{count, plural, =1 {إضافة عنصر} two {إضافة عنصرين} other {إضافة {count} عنصرًا}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
@@ -826,7 +826,7 @@
"referFriendsAnd2xYourPlan": "أحِل الأصدقاء وضاعف خطتك مرتين",
"shareAlbumHint": "افتح ألبومًا وانقر على زر المشاركة في الزاوية اليمنى العليا للمشاركة.",
"itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "تعرض العناصر عدد الأيام المتبقية قبل الحذف الدائم.",
"trashDaysLeft": "{count, plural, =0 {قريبًا} =1 {يوم واحد} two {يومان} few {{count} أيام} many {{count} يومًا} other {{count} يومًا}}",
"trashDaysLeft": "{count, plural, =0 {قريبًا} =1 {يوم واحد} two {يومان} other {{count} يومًا}}",
"@trashDaysLeft": {
"description": "Text to indicate number of days remaining before permanent deletion",
"placeholders": {
@@ -899,8 +899,8 @@
"authToViewYourMemories": "يرجى المصادقة لعرض ذكرياتك.",
"unlock": "فتح",
"freeUpSpace": "تحرير المساحة",
"freeUpSpaceSaving": "{count, plural, =1 {يمكن حذفه من الجهاز لتحرير {formattedSize}} two {يمكن حذفهما من الجهاز لتحرير {formattedSize}} few {يمكن حذفها من الجهاز لتحرير {formattedSize}} many {يمكن حذفها من الجهاز لتحرير {formattedSize}} other {يمكن حذفها من الجهاز لتحرير {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {ملف واحد} two {ملفان} few {{formattedNumber} ملفات} many {{formattedNumber} ملفًا} other {{formattedNumber} ملفًا}} في هذا الألبوم تم نسخه احتياطيًا بأمان",
"freeUpSpaceSaving": "{count, plural, =1 {يمكن حذفه من الجهاز لتحرير {formattedSize}} two {يمكن حذفهما من الجهاز لتحرير {formattedSize}} other {يمكن حذفها من الجهاز لتحرير {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {ملف واحد} two {ملفان} other {{formattedNumber} ملفًا}} في هذا الألبوم تم نسخه احتياطيًا بأمان",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
"placeholders": {
@@ -915,7 +915,7 @@
}
}
},
"filesBackedUpFromDevice": "{count, plural, one {ملف واحد} two {ملفان} few {{formattedNumber} ملفات} many {{formattedNumber} ملفًا} other {{formattedNumber} ملفًا}} على هذا الجهاز تم نسخه احتياطيًا بأمان",
"filesBackedUpFromDevice": "{count, plural, one {ملف واحد} two {ملفان} other {{formattedNumber} ملفًا}} على هذا الجهاز تم نسخه احتياطيًا بأمان",
"@filesBackedUpFromDevice": {
"description": "Text to tell user how many files have been backed up from this device",
"placeholders": {
@@ -1217,7 +1217,7 @@
"searchHint4": "الموقع",
"searchHint5": "قريبًا: الوجوه والبحث السحري ✨",
"addYourPhotosNow": "أضف صورك الآن",
"searchResultCount": "{count, plural, one{{count} النتائج التي تم العثور عليها} other{{count} النتائج التي تم العثور عليها}}",
"searchResultCount": "{count, plural, other{{count} النتائج التي تم العثور عليها}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1269,8 +1269,8 @@
"description": "Subtitle to indicate that the user can find people quickly by name"
},
"findPeopleByName": "البحث عن الأشخاص بسرعة بالاسم",
"addViewers": "{count, plural, =0 {إضافة مشاهد} =1 {إضافة مشاهد} two {إضافة مشاهدين} few {إضافة {count} مشاهدين} many {إضافة {count} مشاهدًا} other {إضافة {count} مشاهدًا}}",
"addCollaborators": "{count, plural, =0 {إضافة متعاون} =1 {إضافة متعاون} two {إضافة متعاونين} few {إضافة {count} متعاونين} many {إضافة {count} متعاونًا} other {إضافة {count} متعاونًا}}",
"addViewers": "{count, plural, =0 {إضافة مشاهد} =1 {إضافة مشاهد} two {إضافة مشاهدين} other {إضافة {count} مشاهدًا}}",
"addCollaborators": "{count, plural, =0 {إضافة متعاون} =1 {إضافة متعاون} two {إضافة متعاونين} other {إضافة {count} متعاونًا}}",
"longPressAnEmailToVerifyEndToEndEncryption": "اضغط مطولاً على بريد إلكتروني للتحقق من التشفير من طرف إلى طرف.",
"developerSettingsWarning": "هل أنت متأكد من رغبتك في تعديل إعدادات المطور؟",
"developerSettings": "إعدادات المطور",
@@ -1403,7 +1403,7 @@
"enableMachineLearningBanner": "قم بتمكين تعلم الآلة للبحث السحري والتعرف على الوجوه.",
"searchDiscoverEmptySection": "سيتم عرض الصور هنا بمجرد اكتمال المعالجة والمزامنة.",
"searchPersonsEmptySection": "سيتم عرض الأشخاص هنا بمجرد اكتمال المعالجة والمزامنة.",
"viewersSuccessfullyAdded": "{count, plural, =0 {تمت إضافة 0 مشاهدين} =1 {تمت إضافة مشاهد واحد} two {تمت إضافة مشاهدين} few {تمت إضافة {count} مشاهدين} many {تمت إضافة {count} مشاهدًا} other {تمت إضافة {count} مشاهدًا}}",
"viewersSuccessfullyAdded": "{count, plural, =0 {تمت إضافة 0 مشاهدين} =1 {تمت إضافة مشاهد واحد} two {تمت إضافة مشاهدين} other {تمت إضافة {count} مشاهدًا}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1413,7 +1413,7 @@
},
"description": "Number of viewers that were successfully added to an album."
},
"collaboratorsSuccessfullyAdded": "{count, plural, =0 {تمت إضافة 0 متعاونين} =1 {تمت إضافة متعاون واحد} two {تمت إضافة متعاونين} few {تمت إضافة {count} متعاونين} many {تمت إضافة {count} متعاونًا} other {تمت إضافة {count} متعاونًا}}",
"collaboratorsSuccessfullyAdded": "{count, plural, =0 {تمت إضافة 0 متعاونين} =1 {تمت إضافة متعاون واحد} two {تمت إضافة متعاونين} other {تمت إضافة {count} متعاونًا}}",
"@collaboratorsSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1488,7 +1488,7 @@
},
"currentlyRunning": "قيد التشغيل حاليًا",
"ignored": "تم التجاهل",
"photosCount": "{count, plural, =0 {لا توجد صور} =1 {صورة واحدة} two {صورتان} few {{count} صور} many {{count} صورة} other {{count} صورة}}",
"photosCount": "{count, plural, =0 {لا توجد صور} =1 {صورة واحدة} two {صورتان} other {{count} صورة}}",
"@photosCount": {
"placeholders": {
"count": {
@@ -1686,7 +1686,7 @@
"moveSelectedPhotosToOneDate": "نقل الصور المحددة إلى تاريخ واحد",
"shiftDatesAndTime": "تغيير التواريخ والوقت",
"photosKeepRelativeTimeDifference": "تحتفظ الصور بالفرق الزمني النسبي",
"photocountPhotos": "{count, plural, =0 {لا توجد صور} =1 {صورة واحدة} two {صورتان} few {{count} صور} many {{count} صورة} other {{count} صورة}}",
"photocountPhotos": "{count, plural, =0 {لا توجد صور} =1 {صورة واحدة} two {صورتان} other {{count} صورة}}",
"@photocountPhotos": {
"placeholders": {
"count": {
@@ -1700,7 +1700,7 @@
"selectedItemsWillBeRemovedFromThisPerson": "سيتم إزالة العناصر المحددة من هذا الشخص، ولكن لن يتم حذفها من مكتبتك.",
"throughTheYears": "{dateFormat} عبر السنين",
"thisWeekThroughTheYears": "هذا الأسبوع عبر السنين",
"thisWeekXYearsAgo": "{count, plural, =1 {هذا الأسبوع، قبل سنة} two {هذا الأسبوع، قبل سنتين} few {هذا الأسبوع، قبل {count} سنوات} many {هذا الأسبوع، قبل {count} سنة} other {هذا الأسبوع، قبل {count} سنة}}",
"thisWeekXYearsAgo": "{count, plural, =1 {هذا الأسبوع، قبل سنة} two {هذا الأسبوع، قبل سنتين} other {هذا الأسبوع، قبل {count} سنة}}",
"youAndThem": "أنت و {name}",
"admiringThem": "الإعجاب بـ {name}",
"embracingThem": "معانقة {name}",
@@ -1821,4 +1821,4 @@
}
}
}
}
}

View File

@@ -372,7 +372,7 @@
"deleteFromBoth": "Odstranit z obou",
"newAlbum": "Nové album",
"albums": "Alba",
"memoryCount": "{count, plural, =0{žádné vzpomínky} one{{formattedCount} vzpomínka} few{{formattedCount} vzpomínky} other{{formattedCount} vzpomínek}}",
"memoryCount": "{count, plural, =0{žádné vzpomínky} one{{formattedCount} vzpomínka} other{{formattedCount} vzpomínek}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -459,8 +459,8 @@
"selectAll": "Vybrat vše",
"skip": "Přeskočit",
"updatingFolderSelection": "Aktualizuji výběr složek...",
"itemCount": "{count, plural, one{{count} položka} few{{count} položky} other{{count} položek}}",
"deleteItemCount": "{count, plural, =1 {Smazat {count} položku} few{Smazat {count} položky} other {Smazat {count} položek}}",
"itemCount": "{count, plural, one{{count} položka} other{{count} položek}}",
"deleteItemCount": "{count, plural, =1 {Smazat {count} položku} other {Smazat {count} položek}}",
"duplicateItemsGroup": "{count} souborů, {formattedSize} každý",
"@duplicateItemsGroup": {
"description": "Display the number of duplicate files and their size",
@@ -543,7 +543,7 @@
},
"remindToEmptyEnteTrash": "Vyprázdněte také \"Koš\", abyste získali uvolněné místo",
"sparkleSuccess": "✨ Úspěch",
"duplicateFileCountWithStorageSaved": "Vyčistili jste {count, plural, one{{count} duplicitní soubor} few{{count} duplicitní soubory} other{{count} duplicitních souborů}}, a ušetřili jste {storageSaved}!",
"duplicateFileCountWithStorageSaved": "Vyčistili jste {count, plural, one{{count} duplicitní soubor} other{{count} duplicitních souborů}}, a ušetřili jste {storageSaved}!",
"@duplicateFileCountWithStorageSaved": {
"description": "The text to display when the user has successfully cleaned up duplicate files",
"type": "text",
@@ -826,7 +826,7 @@
"referFriendsAnd2xYourPlan": "Doporučte přátele a zdvojnásobte svůj tarif",
"shareAlbumHint": "Otevřete album a klepněte na tlačítko sdílení v pravém horním rohu, abyste jej sdíleli.",
"itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Položky zobrazují počet dní zbývajících do trvalého smazání",
"trashDaysLeft": "{count, plural, =0{Brzy} =1{1 den} few{{count} dny} other{{count} dní}}",
"trashDaysLeft": "{count, plural, =0{Brzy} =1{1 den} other{{count} dní}}",
"@trashDaysLeft": {
"description": "Text to indicate number of days remaining before permanent deletion",
"placeholders": {
@@ -900,7 +900,7 @@
"unlock": "Odemknout",
"freeUpSpace": "Uvolnit místo",
"freeUpSpaceSaving": "{count, plural, =1 {Lze jej odstranit ze zařízení, aby se uvolnilo {formattedSize} místa} other {Lze je odstranit ze zařízení, aby se uvolnilo {formattedSize} místa}}",
"filesBackedUpInAlbum": "{count, plural, one {1 soubor v tomto albu byl bezpečně zálohován} few {{formattedNumber} soubory v tomto albu byly bezpečně zálohovány} other {{formattedNumber} souborů v tomto albu bylo bezpečně zálohováno}}",
"filesBackedUpInAlbum": "{count, plural, one {1 soubor v tomto albu byl bezpečně zálohován} other {{formattedNumber} souborů v tomto albu bylo bezpečně zálohováno}}",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
"placeholders": {
@@ -915,7 +915,7 @@
}
}
},
"filesBackedUpFromDevice": "{count, plural, one {1 soubor na tomto zařízení byl bezpečně zálohován} few {{formattedNumber} soubory na tomto zařízení byly bezpečně zálohovány} other {{formattedNumber} souborů na tomto zařízení bylo bezpečně zálohováno}}",
"filesBackedUpFromDevice": "{count, plural, one {1 soubor na tomto zařízení byl bezpečně zálohován} other {{formattedNumber} souborů na tomto zařízení bylo bezpečně zálohováno}}",
"@filesBackedUpFromDevice": {
"description": "Text to tell user how many files have been backed up from this device",
"placeholders": {
@@ -1217,7 +1217,7 @@
"searchHint4": "Poloha",
"searchHint5": "Již brzy: Kouzelné vyhledávání tváří ✨",
"addYourPhotosNow": "Přidejte své fotografie nyní",
"searchResultCount": "{count, plural, one{Nalezen {count} výsledek} few{Nalezeny {count} výsledky} other{Nalezeno {count} výsledků}}",
"searchResultCount": "{count, plural, one{Nalezen {count} výsledek} other{Nalezeno {count} výsledků}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1403,7 +1403,7 @@
"enableMachineLearningBanner": "Povolte strojové učení pro magické vyhledávání a rozpoznávání obličejů",
"searchDiscoverEmptySection": "Obrázky se zde zobrazí po dokončení zpracování a synchronizace",
"searchPersonsEmptySection": "Lidé se zde zobrazí po dokončení zpracování a synchronizace",
"viewersSuccessfullyAdded": "{count, plural, =0 {Přidáno 0 pozorovatelů} =1 {Přidán 1 pozorovatel} few {Přidáni {count} pozorovatelé} other {Přidáno {count} pozorovatelů}}",
"viewersSuccessfullyAdded": "{count, plural, =0 {Přidáno 0 pozorovatelů} =1 {Přidán 1 pozorovatel} other {Přidáno {count} pozorovatelů}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1413,7 +1413,7 @@
},
"description": "Number of viewers that were successfully added to an album."
},
"collaboratorsSuccessfullyAdded": "{count, plural, =0 {Přidáno 0 spolupracovníků} =1 {Přidán 1 spolupracovník} few {Přidáni {count} spolupracovníci} other {Přidáno {count} spolupracovníků}}",
"collaboratorsSuccessfullyAdded": "{count, plural, =0 {Přidáno 0 spolupracovníků} =1 {Přidán 1 spolupracovník} other {Přidáno {count} spolupracovníků}}",
"@collaboratorsSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1488,7 +1488,7 @@
},
"currentlyRunning": "aktuálně běží",
"ignored": "ignorováno",
"photosCount": "{count, plural, =0 {0 fotografií} =1 {1 fotografie} few {{count} fotografie} other {{count} fotografií}}",
"photosCount": "{count, plural, =0 {0 fotografií} =1 {1 fotografie} other {{count} fotografií}}",
"@photosCount": {
"placeholders": {
"count": {
@@ -1686,7 +1686,7 @@
"moveSelectedPhotosToOneDate": "Přesunout vybrané fotografie do jednoho data",
"shiftDatesAndTime": "Posunout datum a čas",
"photosKeepRelativeTimeDifference": "Fotografie zachovávají relativní časový rozdíl",
"photocountPhotos": "{count, plural, =0 {Žádné fotografie} =1 {1 fotografie} few {{count} fotografie} other {{count} fotografií}}",
"photocountPhotos": "{count, plural, =0 {Žádné fotografie} =1 {1 fotografie} other {{count} fotografií}}",
"@photocountPhotos": {
"placeholders": {
"count": {
@@ -1700,7 +1700,7 @@
"selectedItemsWillBeRemovedFromThisPerson": "Vybrané položky budou z této osoby odebrány, ale nebudou smazány z vaší knihovny.",
"throughTheYears": "{dateFormat} v průběhu let",
"thisWeekThroughTheYears": "Tento týden v průběhu let",
"thisWeekXYearsAgo": "{count, plural, =1 {Tento týden, {count} rok nazpět} few {Tento týden, {count} roky nazpět} other {Tento týden, {count} let nazpět}}",
"thisWeekXYearsAgo": "{count, plural, =1 {Tento týden, {count} rok nazpět} other {Tento týden, {count} let nazpět}}",
"youAndThem": "Vy a {name}",
"admiringThem": "Obdiv k {name}",
"embracingThem": "Objímání {name}",
@@ -1844,7 +1844,7 @@
"findSimilarImages": "Najít podobné obrázky",
"noSimilarImagesFound": "Nebyly nalezeny žádné podobné obrázky",
"yourPhotosLookUnique": "Vaše fotografie vypadají jedinečně",
"similarGroupsFound": "{count, plural, =1{{count} skupina nalezena} few{{count} skupiny nalezeny} other{{count} skupin nalezeno}}",
"similarGroupsFound": "{count, plural, =1{{count} skupina nalezena} other{{count} skupin nalezeno}}",
"@similarGroupsFound": {
"placeholders": {
"count": {
@@ -1921,4 +1921,4 @@
"similar": "Podobné",
"identical": "Identické",
"nothingHereTryAnotherFilter": "Tady nic není, zkuste jiný filtr! 👀"
}
}

View File

@@ -207,16 +207,6 @@
"after1Month": "Efter 1 måned",
"after1Year": "Efter 1 år",
"manageParticipants": "Administrer",
"albumParticipantsCount": "{count, plural, =0 {Ingen Deltagere} =1 {1 Deltager} other {{count} Deltagere}}",
"@albumParticipantsCount": {
"placeholders": {
"count": {
"type": "int",
"example": "5"
}
},
"description": "Number of participants in an album, including the album owner."
},
"collabLinkSectionDescription": "Opret et link, så folk kan tilføje og se fotos i dit delte album uden at behøve en Ente-app eller konto. Fantastisk til at indsamle event fotos.",
"collectPhotos": "Indsaml billeder",
"collaborativeLink": "Kollaborativt link",

View File

@@ -1819,7 +1819,7 @@
"font": "Fuente",
"background": "Fondo",
"align": "Alinear",
"addedToAlbums": "{count, plural, one {}=1{Añadido con éxito a 1 álbum} other{Añadido con éxito a {count} álbumes}}",
"addedToAlbums": "{count, plural, =1{Añadido con éxito a 1 álbum} other{Añadido con éxito a {count} álbumes}}",
"@addedToAlbums": {
"description": "Message shown when items are added to albums",
"placeholders": {
@@ -1846,7 +1846,7 @@
"findSimilarImages": "Buscar imágenes similares",
"noSimilarImagesFound": "No se encontraron imágenes similares",
"yourPhotosLookUnique": "Tus fotos se ven únicas",
"similarGroupsFound": "{count, plural, one {}=1{{count} grupo encontrado} other{{count} grupos encontrados}}",
"similarGroupsFound": "{count, plural, =1{{count} grupo encontrado} other{{count} grupos encontrados}}",
"@similarGroupsFound": {
"placeholders": {
"count": {
@@ -1934,4 +1934,4 @@
"related": "Relacionado",
"hoorayyyy": "¡Hurraaaa!",
"nothingToTidyUpHere": "Nada que limpiar aquí"
}
}

View File

@@ -899,7 +899,7 @@
"authToViewYourMemories": "Authentifiez-vous pour voir vos souvenirs",
"unlock": "Déverrouiller",
"freeUpSpace": "Libérer de l'espace",
"freeUpSpaceSaving": "{count, plural, one {}=1 {Il peut être supprimé de l'appareil pour libérer {formattedSize}} other {Ils peuvent être supprimés de l'appareil pour libérer {formattedSize}}}",
"freeUpSpaceSaving": "{count, plural, =1 {Il peut être supprimé de l'appareil pour libérer {formattedSize}} other {Ils peuvent être supprimés de l'appareil pour libérer {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {1 fichier dans cet album a été sauvegardé en toute sécurité} other {{formattedNumber} fichiers dans cet album ont été sauvegardés en toute sécurité}}",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
@@ -933,7 +933,7 @@
"@freeUpSpaceSaving": {
"description": "Text to tell user how much space they can free up by deleting items from the device"
},
"freeUpAccessPostDelete": "Vous pouvez toujours {count, plural, one {}=1 {l'} other {les}} accéder sur Ente tant que vous avez un abonnement actif",
"freeUpAccessPostDelete": "Vous pouvez toujours {count, plural, =1 {l'} other {les}} accéder sur Ente tant que vous avez un abonnement actif",
"@freeUpAccessPostDelete": {
"placeholders": {
"count": {
@@ -1269,8 +1269,8 @@
"description": "Subtitle to indicate that the user can find people quickly by name"
},
"findPeopleByName": "Trouver des personnes rapidement par leur nom",
"addViewers": "{count, plural, one {}=0 {Ajouter un spectateur} =1 {Ajouter une spectateur} other {Ajouter des spectateurs}}",
"addCollaborators": "{count, plural, one {}=0 {Ajouter un collaborateur} =1 {Ajouter un collaborateur} other {Ajouter des collaborateurs}}",
"addViewers": "{count, plural, =0 {Ajouter un spectateur} =1 {Ajouter une spectateur} other {Ajouter des spectateurs}}",
"addCollaborators": "{count, plural, =0 {Ajouter un collaborateur} =1 {Ajouter un collaborateur} other {Ajouter des collaborateurs}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Appuyez longuement sur un email pour vérifier le chiffrement de bout en bout.",
"developerSettingsWarning": "Êtes-vous sûr de vouloir modifier les paramètres du développeur ?",
"developerSettings": "Paramètres du développeur",
@@ -1403,7 +1403,7 @@
"enableMachineLearningBanner": "Activer l'apprentissage automatique pour la reconnaissance des visages et la recherche magique",
"searchDiscoverEmptySection": "Les images seront affichées ici une fois le traitement terminé",
"searchPersonsEmptySection": "Les personnes seront affichées ici une fois le traitement terminé",
"viewersSuccessfullyAdded": "{count, plural, one {}=0 {0 spectateur ajouté} =1 {Un spectateur ajouté} other {{count} spectateurs ajoutés}}",
"viewersSuccessfullyAdded": "{count, plural, =0 {0 spectateur ajouté} =1 {Un spectateur ajouté} other {{count} spectateurs ajoutés}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1819,7 +1819,7 @@
"font": "Police",
"background": "Arrière-plan",
"align": "Aligner",
"addedToAlbums": "{count, plural, one {}=1{Ajouté avec succès à 1 album} other{Ajouté avec succès à {count} albums}}",
"addedToAlbums": "{count, plural, =1{Ajouté avec succès à 1 album} other{Ajouté avec succès à {count} albums}}",
"@addedToAlbums": {
"description": "Message shown when items are added to albums",
"placeholders": {
@@ -1926,4 +1926,4 @@
"related": "Liés",
"hoorayyyy": "Houraaa !",
"nothingToTidyUpHere": "Rien à nettoyer ici"
}
}

View File

@@ -389,8 +389,8 @@
"selectAll": "בחר הכל",
"skip": "דלג",
"updatingFolderSelection": "מעדכן את בחירת התיקיות...",
"itemCount": "{count, plural, one{{count} פריט} two {{count} פריטים} many {{count} פריטים} other{{count} פריטים}}",
"deleteItemCount": "{count, plural, =1 {מחק {count} פריט} two {מחק {count} פריטים} other {מחק {count} פריטים}}",
"itemCount": "{count, plural, one{{count} פריט} other{{count} פריטים}}",
"deleteItemCount": "{count, plural, =1 {מחק {count} פריט} other {מחק {count} פריטים}}",
"duplicateItemsGroup": "{count} קבצים, כל אחד {formattedSize}",
"@duplicateItemsGroup": {
"description": "Display the number of duplicate files and their size",
@@ -407,7 +407,7 @@
}
},
"showMemories": "הצג זכרונות",
"yearsAgo": "{count, plural, one{לפני {count} שנה} two {לפני {count} שנים} many {לפני {count} שנים} other{לפני {count} שנים}}",
"yearsAgo": "{count, plural, one{לפני {count} שנה} other{לפני {count} שנים}}",
"backupSettings": "הגדרות גיבוי",
"backupOverMobileData": "גבה על רשת סלולרית",
"backupVideos": "גבה סרטונים",
@@ -792,4 +792,4 @@
"create": "צור",
"viewAll": "הצג הכל",
"hiding": "מחביא..."
}
}

View File

@@ -458,7 +458,7 @@
"selectAll": "Összes kijelölése",
"skip": "Kihagyás",
"updatingFolderSelection": "Mappakijelölés frissítése...",
"itemCount": "{count, plural, one{{count} elem} other{{count} elem}}",
"itemCount": "{count, plural, other{{count} elem}}",
"deleteItemCount": "{count, plural, =1 {Elem {count} törlése} other {Elemek {count} törlése}}",
"duplicateItemsGroup": "{count} fájl, {formattedSize} mindegyik",
"@duplicateItemsGroup": {
@@ -541,4 +541,4 @@
}
},
"remindToEmptyEnteTrash": "Ürítsd ki a \"Kukát\" is, hogy visszaszerezd a felszabadult helyet."
}
}

View File

@@ -372,7 +372,7 @@
"deleteFromBoth": "Hapus dari keduanya",
"newAlbum": "Album baru",
"albums": "Album",
"memoryCount": "{count, plural, =0{tidak ada memori} one{{formattedCount} memori} other{{formattedCount} memori}}",
"memoryCount": "{count, plural, =0{tidak ada memori} other{{formattedCount} memori}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -1234,4 +1234,4 @@
"left": "Kiri",
"right": "Kanan",
"whatsNew": "Hal yang baru"
}
}

View File

@@ -794,11 +794,11 @@
"share": "Condividi",
"unhideToAlbum": "Non nascondere l'album",
"restoreToAlbum": "Ripristina l'album",
"moveItem": "{count, plural, one {}=1 {Sposta elemento} other {Sposta elementi}}",
"moveItem": "{count, plural, =1 {Sposta elemento} other {Sposta elementi}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, one {}=1 {Aggiungi elemento} other {Aggiungi elementi}}",
"addItem": "{count, plural, =1 {Aggiungi elemento} other {Aggiungi elementi}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
@@ -899,7 +899,7 @@
"authToViewYourMemories": "Autenticati per visualizzare le tue foto",
"unlock": "Sblocca",
"freeUpSpace": "Libera spazio",
"freeUpSpaceSaving": "{count, plural, one {}=1 {Può essere cancellato per liberare {formattedSize}} other {Possono essere cancellati per liberare {formattedSize}}}",
"freeUpSpaceSaving": "{count, plural, =1 {Può essere cancellato per liberare {formattedSize}} other {Possono essere cancellati per liberare {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {1 file} other {{formattedNumber} file}} di quest'album sono stati salvati in modo sicuro",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
@@ -1260,8 +1260,8 @@
"description": "Subtitle to indicate that the user can find people quickly by name"
},
"findPeopleByName": "Trova rapidamente le persone per nome",
"addViewers": "{count, plural, one {}=0 {Aggiungi visualizzatore} =1 {Add viewer} other {Aggiungi visualizzatori}}",
"addCollaborators": "{count, plural, one {}=0 {Aggiungi collaboratore} =1 {Aggiungi collaboratore} other {Aggiungi collaboratori}}",
"addViewers": "{count, plural, =0 {Aggiungi visualizzatore} =1 {Add viewer} other {Aggiungi visualizzatori}}",
"addCollaborators": "{count, plural, =0 {Aggiungi collaboratore} =1 {Aggiungi collaboratore} other {Aggiungi collaboratori}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Premi a lungo un'email per verificare la crittografia end to end.",
"developerSettingsWarning": "Sei sicuro di voler modificare le Impostazioni sviluppatore?",
"developerSettings": "Impostazioni sviluppatore",
@@ -1394,7 +1394,7 @@
"enableMachineLearningBanner": "Abilita l'apprendimento automatico per la ricerca magica e il riconoscimento facciale",
"searchDiscoverEmptySection": "Le immagini saranno mostrate qui una volta che l'elaborazione e la sincronizzazione saranno completate",
"searchPersonsEmptySection": "Le persone saranno mostrate qui una volta che l'elaborazione e la sincronizzazione saranno completate",
"viewersSuccessfullyAdded": "{count, plural, one {}=0 {Added 0 visualizzatori} =1 {Added 1 visualizzatore} other {Added {count} visualizzatori}}",
"viewersSuccessfullyAdded": "{count, plural, =0 {Added 0 visualizzatori} =1 {Added 1 visualizzatore} other {Added {count} visualizzatori}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1479,7 +1479,7 @@
},
"currentlyRunning": "attualmente in esecuzione",
"ignored": "ignorato",
"photosCount": "{count, plural, one {}=0 {0 foto} =1 {1 foto} other {{count} foto}}",
"photosCount": "{count, plural, =0 {0 foto} =1 {1 foto} other {{count} foto}}",
"@photosCount": {
"placeholders": {
"count": {
@@ -1677,7 +1677,7 @@
"moveSelectedPhotosToOneDate": "Sposta foto selezionate in una data specifica",
"shiftDatesAndTime": "Sposta date e orari",
"photosKeepRelativeTimeDifference": "Le foto mantengono una differenza di tempo relativa",
"photocountPhotos": "{count, plural, one {}=0 {Nessuna foto} =1 {1 foto} other {{count} foto}}",
"photocountPhotos": "{count, plural, =0 {Nessuna foto} =1 {1 foto} other {{count} foto}}",
"@photocountPhotos": {
"placeholders": {
"count": {
@@ -1691,7 +1691,7 @@
"selectedItemsWillBeRemovedFromThisPerson": "Gli elementi selezionati verranno rimossi da questa persona, ma non eliminati dalla tua libreria.",
"throughTheYears": "{dateFormat} negli anni",
"thisWeekThroughTheYears": "Questa settimana negli anni",
"thisWeekXYearsAgo": "{count, plural, one {}=1 {Questa settimana, {count} anno fa} other {Questa settimana, {count} anni fa}}",
"thisWeekXYearsAgo": "{count, plural, =1 {Questa settimana, {count} anno fa} other {Questa settimana, {count} anni fa}}",
"youAndThem": "Tu e {name}",
"admiringThem": "Ammirando {name}",
"embracingThem": "Abbracciando {name}",
@@ -1746,4 +1746,4 @@
"receiveRemindersOnBirthdays": "Ricevi promemoria quando è il compleanno di qualcuno. Toccare la notifica ti porterà alle foto della persona che compie gli anni.",
"happyBirthday": "Buon compleanno! 🥳",
"birthdays": "Compleanni"
}
}

View File

@@ -461,7 +461,7 @@
}
},
"showMemories": "思い出を表示",
"yearsAgo": "{count, plural, one{{count} 年前} other{{count} 年前}}",
"yearsAgo": "{count, plural, other{{count} 年前}}",
"backupSettings": "バックアップ設定",
"backupStatus": "バックアップの状態",
"backupStatusDescription": "バックアップされたアイテムがここに表示されます",
@@ -527,7 +527,7 @@
},
"remindToEmptyEnteTrash": "「ゴミ箱」も空にするとアカウントのストレージが解放されます",
"sparkleSuccess": "成功✨",
"duplicateFileCountWithStorageSaved": "お掃除しました {count, plural, one{{count} 個の重複ファイル} other{{count} 個の重複ファイル}}, ({storageSaved}が開放されます!)",
"duplicateFileCountWithStorageSaved": "お掃除しました {count, plural, other{{count} 個の重複ファイル}}, ({storageSaved}が開放されます!)",
"@duplicateFileCountWithStorageSaved": {
"description": "The text to display when the user has successfully cleaned up duplicate files",
"type": "text",
@@ -1178,7 +1178,7 @@
"searchHint4": "場所",
"searchHint5": "近日公開: フェイスとマジック検索 ✨",
"addYourPhotosNow": "写真を今すぐ追加する",
"searchResultCount": "{count, plural, one{{count} 個の結果} other{{count} 個の結果}}",
"searchResultCount": "{count, plural, other{{count} 個の結果}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1666,4 +1666,4 @@
"onTheRoad": "再び道で",
"food": "料理を楽しむ",
"pets": "毛むくじゃらな仲間たち"
}
}

View File

@@ -794,11 +794,7 @@
"share": "Bendrinti",
"unhideToAlbum": "Rodyti į albumą",
"restoreToAlbum": "Atkurti į albumą",
"moveItem": "{count, plural, =1 {Perkelti elementą} other {Perkelti elementų}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, =1 {Pridėti elementą} other {Pridėti elementų}}",
"addItem": "{count, plural, =1 {Pridėti elementą} other {Pridėti elementų}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
@@ -900,7 +896,7 @@
"unlock": "Atrakinti",
"freeUpSpace": "Atlaisvinti vietos",
"freeUpSpaceSaving": "{count, plural, =1 {Jį galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}} other {Jų galima ištrinti iš įrenginio, kad atlaisvintų {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija} few {{formattedNumber} failai šiame albume saugiai sukurtos atsarginės kopijos} many {{formattedNumber} failo šiame albume saugiai sukurtos atsargines kopijos} other {{formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija}}.",
"filesBackedUpInAlbum": "{count, plural, one {{formattedNumber} failas šiame albume saugiai sukurta atsarginė kopija} other {{formattedNumber} failų šiame albume saugiai sukurta atsarginė kopija}}.",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
"placeholders": {
@@ -915,7 +911,7 @@
}
}
},
"filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} failas šiame įrenginyje saugiai sukurta atsarginė kopija} few {{formattedNumber} failai šiame įrenginyje saugiai sukurtos atsarginės kopijos} many {{formattedNumber} failo šiame įrenginyje saugiai sukurtos atsargines kopijos} other {{formattedNumber} failų šiame įrenginyje saugiai sukurta atsarginių kopijų}}.",
"filesBackedUpFromDevice": "{count, plural, one {{formattedNumber} failas šiame įrenginyje saugiai sukurta atsarginė kopija} other {{formattedNumber} failų šiame įrenginyje saugiai sukurta atsarginių kopijų}}.",
"@filesBackedUpFromDevice": {
"description": "Text to tell user how many files have been backed up from this device",
"placeholders": {
@@ -1403,7 +1399,7 @@
"enableMachineLearningBanner": "Įjunkite mašininį mokymąsi magiškai paieškai ir veidų atpažinimui",
"searchDiscoverEmptySection": "Vaizdai bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.",
"searchPersonsEmptySection": "Asmenys bus rodomi čia, kai bus užbaigtas apdorojimas ir sinchronizavimas.",
"viewersSuccessfullyAdded": "{count, plural, one {Įtrauktas {count} žiūrėtojas} few {Įtraukti {count} žiūrėtojai} many {Įtraukta {count} žiūrėtojo} =0 {Įtraukta 0 žiūrėtojų} =1 {Įtrauktas 1 žiūrėtojas} other {Įtraukta {count} žiūrėtojų}}",
"viewersSuccessfullyAdded": "{count, plural, =0 {Įtraukta 0 žiūrėtojų} =1 {Įtrauktas 1 žiūrėtojas} other {Įtraukta {count} žiūrėtojų}}",
"@viewersSuccessfullyAdded": {
"placeholders": {
"count": {
@@ -1488,7 +1484,7 @@
},
"currentlyRunning": "šiuo metu vykdoma",
"ignored": "ignoruota",
"photosCount": "{count, plural, one {{count} nuotrauka} few {{count} nuotraukos} many {{count} nuotraukos} =0 {0 nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}",
"photosCount": "{count, plural, =0 {0 nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}",
"@photosCount": {
"placeholders": {
"count": {
@@ -1686,21 +1682,11 @@
"moveSelectedPhotosToOneDate": "Perkelti pasirinktas nuotraukas į vieną datą",
"shiftDatesAndTime": "Pastumti datas ir laiką",
"photosKeepRelativeTimeDifference": "Nuotraukos išlaiko santykinį laiko skirtumą",
"photocountPhotos": "{count, plural, =0 {Nėra nuotraukų} =1 {1 nuotrauka} other {{count} nuotraukų}}",
"@photocountPhotos": {
"placeholders": {
"count": {
"type": "int",
"example": "2"
}
}
},
"appIcon": "Programos piktograma",
"notThisPerson": "Ne šis asmuo?",
"selectedItemsWillBeRemovedFromThisPerson": "Pasirinkti elementai bus pašalinti iš šio asmens, bet nebus ištrinti iš jūsų bibliotekos.",
"throughTheYears": "{dateFormat} per metus",
"thisWeekThroughTheYears": "Ši savaitė per metus",
"thisWeekXYearsAgo": "{count, plural, =1 {Šią savaitę, prieš {count} metus} other {Šią savaitę, prieš {count} metų}}",
"youAndThem": "Jūs ir {name}",
"admiringThem": "Žavisi {name}",
"embracingThem": "Apkabinat {name}",
@@ -1830,4 +1816,4 @@
"size": "Dydis",
"similarity": "Panašumas",
"processingLocally": "Apdorojama vietoje"
}
}

View File

@@ -477,7 +477,7 @@
}
},
"showMemories": "Toon herinneringen",
"yearsAgo": "{count, plural, one{{count} jaar geleden} other{{count} jaar geleden}}",
"yearsAgo": "{count, plural, other{{count} jaar geleden}}",
"backupSettings": "Back-up instellingen",
"backupStatus": "Back-up status",
"backupStatusDescription": "Items die zijn geback-upt, worden hier getoond",
@@ -1773,4 +1773,4 @@
"areYouSureYouWantToMergeThem": "Weet je zeker dat je ze wilt samenvoegen?",
"allUnnamedGroupsWillBeMergedIntoTheSelectedPerson": "Alle naamloze groepen worden samengevoegd met de geselecteerde persoon. Dit kan nog steeds ongedaan worden gemaakt vanuit het geschiedenisoverzicht van de persoon.",
"yesIgnore": "Ja, negeer"
}
}

View File

@@ -171,7 +171,7 @@
"deleteFromBoth": "Slett frå begge",
"newAlbum": "Nytt album",
"albums": "Albums",
"memoryCount": "{count, plural, =0{ingen minne} one{{formattedCount} minne} other{{formattedCount} minne}}",
"memoryCount": "{count, plural, =0{ingen minne} other{{formattedCount} minne}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -310,4 +310,4 @@
"adjust": "Juster",
"draw": "Klistremerke",
"brushColor": "Penselfarge"
}
}

View File

@@ -477,7 +477,7 @@
}
},
"showMemories": "Vis minner",
"yearsAgo": "{count, plural, one{{count} år siden} other{{count} år siden}}",
"yearsAgo": "{count, plural, other{{count} år siden}}",
"backupSettings": "Sikkerhetskopier innstillinger",
"backupStatus": "Status for sikkerhetskopi",
"backupStatusDescription": "Elementer som har blitt sikkerhetskopiert vil vises her",
@@ -1737,4 +1737,4 @@
"memoriesWidgetDesc": "Velg typen minner du ønsker å se på din hjemskjerm.",
"smartMemories": "Smarte minner",
"pastYearsMemories": "Tidligere års minner"
}
}

View File

@@ -372,7 +372,7 @@
"deleteFromBoth": "Usuń z obu",
"newAlbum": "Nowy album",
"albums": "Albumy",
"memoryCount": "{count, plural, =0{brak wspomnień} one{{formattedCount} wspomnienie} few{{formattedCount} wspomnienia} many{{formattedCount} wspomnień} other{{formattedCount} wspomnień}}",
"memoryCount": "{count, plural, =0{brak wspomnień} one{{formattedCount} wspomnienie} other{{formattedCount} wspomnień}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -459,8 +459,8 @@
"selectAll": "Zaznacz wszystko",
"skip": "Pomiń",
"updatingFolderSelection": "Aktualizowanie wyboru folderu...",
"itemCount": "{count, plural, one{{count} element} few {{count} elementy} many {{count} elementów} other{{count} elementu}}",
"deleteItemCount": "{count, plural, =1 {Usuń {count} element} few {Usuń {count} elementy} many {Usuń {count} elementów} other{Usuń {count} elementu}}",
"itemCount": "{count, plural, one{{count} element} other{{count} elementu}}",
"deleteItemCount": "{count, plural, =1 {Usuń {count} element} other{Usuń {count} elementu}}",
"duplicateItemsGroup": "{count} plików, każdy po {formattedSize}",
"@duplicateItemsGroup": {
"description": "Display the number of duplicate files and their size",
@@ -477,7 +477,7 @@
}
},
"showMemories": "Pokaż wspomnienia",
"yearsAgo": "{count, plural, one{{count} rok temu} few {{count} lata temu} many {{count} lat temu} other{{count} lata temu}}",
"yearsAgo": "{count, plural, one{{count} rok temu} other{{count} lata temu}}",
"backupSettings": "Ustawienia kopii zapasowej",
"backupStatus": "Status kopii zapasowej",
"backupStatusDescription": "Elementy, których kopia zapasowa została utworzona, zostaną wyświetlone w tym miejscu",
@@ -794,11 +794,11 @@
"share": "Udostępnij",
"unhideToAlbum": "Odkryj do albumu",
"restoreToAlbum": "Przywróć do albumu",
"moveItem": "{count, plural, =1 {Przenieś element} few {Przenieś elementy} many {Przenieś elementów} other {Przenieś elementów}}",
"moveItem": "{count, plural, =1 {Przenieś element} other {Przenieś elementów}}",
"@moveItem": {
"description": "Page title while moving one or more items to an album"
},
"addItem": "{count, plural, =1 {Dodaj element} few {Dodaj elementy} many {Dodaj elementów} other {Dodaj elementów}}",
"addItem": "{count, plural, =1 {Dodaj element} other {Dodaj elementów}}",
"@addItem": {
"description": "Page title while adding one or more items to album"
},
@@ -826,7 +826,7 @@
"referFriendsAnd2xYourPlan": "Poleć znajomym i podwój swój plan",
"shareAlbumHint": "Otwórz album i dotknij przycisk udostępniania w prawym górnym rogu, aby udostępnić.",
"itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Elementy pokazują liczbę dni pozostałych przed trwałym usunięciem",
"trashDaysLeft": "{count, plural, =0 {Wkrótce} =1{1 dzień} few {{count} dni} other{{count} dni}}",
"trashDaysLeft": "{count, plural, =0 {Wkrótce} =1{1 dzień} other{{count} dni}}",
"@trashDaysLeft": {
"description": "Text to indicate number of days remaining before permanent deletion",
"placeholders": {
@@ -899,7 +899,7 @@
"authToViewYourMemories": "Prosimy uwierzytelnić się, aby wyświetlić swoje wspomnienia",
"unlock": "Odblokuj",
"freeUpSpace": "Zwolnij miejsce",
"freeUpSpaceSaving": "{count, plural, =1 {Może zostać usunięty z urządzenia, aby zwolnić {formattedSize}} many {Może być usuniętych z urządzenia, aby zwolnić {formattedSize}} other {Mogą być usunięte z urządzenia, aby zwolnić {formattedSize}}}",
"freeUpSpaceSaving": "{count, plural, =1 {Może zostać usunięty z urządzenia, aby zwolnić {formattedSize}} other {Mogą być usunięte z urządzenia, aby zwolnić {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {1 plikowi} other {{formattedNumber} plikom}} w tym albumie została bezpiecznie utworzona kopia zapasowa",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
@@ -1217,7 +1217,7 @@
"searchHint4": "Lokalizacja",
"searchHint5": "Wkrótce: Twarze i magiczne wyszukiwanie ✨",
"addYourPhotosNow": "Dodaj swoje zdjęcia teraz",
"searchResultCount": "{count, plural, one{Znaleziono {count} wynik} few {Znaleziono {count} wyniki} other{Znaleziono {count} wyników}}",
"searchResultCount": "{count, plural, one{Znaleziono {count} wynik} other{Znaleziono {count} wyników}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1488,7 +1488,7 @@
},
"currentlyRunning": "aktualnie uruchomiony",
"ignored": "ignorowane",
"photosCount": "{count, plural, =0 {0 zdjęć} =1 {1 zdjęcie} few {{count} zdjęcia} many {{count} zdjęć} other {{count} zdjęć}}",
"photosCount": "{count, plural, =0 {0 zdjęć} =1 {1 zdjęcie} other {{count} zdjęć}}",
"@photosCount": {
"placeholders": {
"count": {
@@ -1686,7 +1686,7 @@
"moveSelectedPhotosToOneDate": "Przenieś wybrane zdjęcia na jedną datę",
"shiftDatesAndTime": "Zmień daty i czas",
"photosKeepRelativeTimeDifference": "Zdjęcia zachowują względną różnicę czasu",
"photocountPhotos": "{count, plural, =0 {Brak zdjęć} =1 {1 zdjęcie} few {{count} zdjęcia} many {{count} zdjęć} other {{count} zdjęć}}",
"photocountPhotos": "{count, plural, =0 {Brak zdjęć} =1 {1 zdjęcie} other {{count} zdjęć}}",
"@photocountPhotos": {
"placeholders": {
"count": {
@@ -1700,7 +1700,7 @@
"selectedItemsWillBeRemovedFromThisPerson": "Wybrane elementy zostaną usunięte z tej osoby, ale nie zostaną usunięte z Twojej biblioteki.",
"throughTheYears": "{dateFormat} przez lata",
"thisWeekThroughTheYears": "Ten tydzień przez lata",
"thisWeekXYearsAgo": "{count, plural, =1 {W tym tygodniu, {count} rok temu} few {W tym tygodniu, {count} lata temu} many {W tym tygodniu, {count} lat temu} other {W tym tygodniu, {count} lat temu}}",
"thisWeekXYearsAgo": "{count, plural, =1 {W tym tygodniu, {count} rok temu} other {W tym tygodniu, {count} lat temu}}",
"youAndThem": "Ty i {name}",
"admiringThem": "Podziwianie {name}",
"embracingThem": "Obejmowanie {name}",
@@ -1828,4 +1828,4 @@
}
}
}
}
}

View File

@@ -1220,15 +1220,17 @@
"@findThemQuickly": {
"description": "Subtitle to indicate that the user can find people quickly by name"
},
"findPeopleByName": "Encontrar pessoas rapidamente pelo nome",
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta.",
"developerSettingsWarning": "Tem a certeza de que pretende modificar as definições de programador?",
"developerSettings": "Definições do programador",
"serverEndpoint": "Endpoint do servidor",
"invalidEndpoint": "Endpoint inválido",
"invalidEndpointMessage": "Desculpe, o endpoint que introduziu é inválido. Introduza um ponto final válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}",
"findPeopleByName": "Busque pessoas facilmente pelo nome",
"addViewers": "{count, plural, one {Adicionar visualizador} other {Adicionar visualizadores}}",
"addCollaborators": "{count, plural, one {Adicionar colaborador} other {Adicionar colaboradores}}",
"longPressAnEmailToVerifyEndToEndEncryption": "Pressione um e-mail para verificar a criptografia ponta a ponta.",
"developerSettingsWarning": "Deseja modificar as Opções de Desenvolvedor?",
"developerSettings": "Opções de desenvolvedor",
"serverEndpoint": "Ponto final do servidor",
"invalidEndpoint": "Ponto final inválido",
"invalidEndpointMessage": "Desculpe, o ponto final inserido é inválido. Insira um ponto final válido e tente novamente.",
"endpointUpdatedMessage": "Ponto final atualizado com sucesso",
"customEndpoint": "Conectado à {endpoint}",
"createCollaborativeLink": "Criar link colaborativo",
"search": "Pesquisar",
"enterPersonName": "Inserir nome da pessoa",

View File

@@ -242,7 +242,7 @@
"publicLinkEnabled": "Link público ativado",
"shareALink": "Partilhar um link",
"sharedAlbumSectionDescription": "Criar álbuns compartilhados e colaborativos com outros usuários da Ente, incluindo usuários em planos gratuitos.",
"shareWithPeopleSectionTitle": "{numberOfPeople, plural, one {}=0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}",
"shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Compartilhe com pessoas específicas} =1 {Compartilhado com 1 pessoa} other {Compartilhado com {numberOfPeople} pessoas}}",
"@shareWithPeopleSectionTitle": {
"placeholders": {
"numberOfPeople": {
@@ -899,7 +899,7 @@
"authToViewYourMemories": "Por favor, autentique-se para ver suas memórias",
"unlock": "Desbloquear",
"freeUpSpace": "Libertar espaço",
"freeUpSpaceSaving": "{count, plural, one {}=1 {Pode eliminá-lo do aparelho para esvaziar {formattedSize}} other {Pode eliminá-los do aparelho para esvaziar {formattedSize}}}",
"freeUpSpaceSaving": "{count, plural, =1 {Pode eliminá-lo do aparelho para esvaziar {formattedSize}} other {Pode eliminá-los do aparelho para esvaziar {formattedSize}}}",
"filesBackedUpInAlbum": "{count, plural, one {1 arquivo} other {{formattedNumber} arquivos}} neste álbum teve um backup seguro",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
@@ -1828,4 +1828,4 @@
}
}
}
}
}

View File

@@ -443,7 +443,7 @@
"selectAll": "Selectare totală",
"skip": "Omiteți",
"updatingFolderSelection": "Se actualizează selecția dosarelor...",
"itemCount": "{count, plural, one{{count} articol} few {{count} articole} other{{count} de articole}}",
"itemCount": "{count, plural, one{{count} articol} other{{count} de articole}}",
"deleteItemCount": "{count, plural, =1 {Ștergeți {count} articol} other {Ștergeți {count} de articole}}",
"duplicateItemsGroup": "{count} fișiere, {formattedSize} fiecare",
"@duplicateItemsGroup": {
@@ -461,7 +461,7 @@
}
},
"showMemories": "Afișare amintiri",
"yearsAgo": "{count, plural, one{acum {count} an} few {acum {count} ani} other{acum {count} de ani}}",
"yearsAgo": "{count, plural, one{acum {count} an} other{acum {count} de ani}}",
"backupSettings": "Setări copie de rezervă",
"backupStatus": "Stare copie de rezervă",
"backupStatusDescription": "Articolele care au fost salvate vor apărea aici",
@@ -526,7 +526,7 @@
},
"remindToEmptyEnteTrash": "De asemenea, goliți „Coșul de gunoi” pentru a revendica spațiul eliberat",
"sparkleSuccess": "✨ Succes",
"duplicateFileCountWithStorageSaved": "Ați curățat {count, plural, one{{count} dublură} few {{count} dubluri} other{{count} de dubluri}}, economisind ({storageSaved}!)",
"duplicateFileCountWithStorageSaved": "Ați curățat {count, plural, one{{count} dublură} other{{count} de dubluri}}, economisind ({storageSaved}!)",
"@duplicateFileCountWithStorageSaved": {
"description": "The text to display when the user has successfully cleaned up duplicate files",
"type": "text",
@@ -873,7 +873,7 @@
"authToViewYourMemories": "Vă rugăm să vă autentificați pentru a vă vizualiza amintirile",
"unlock": "Deblocare",
"freeUpSpace": "Eliberați spațiu",
"filesBackedUpInAlbum": "{count, plural, one {Un fișier din acest album a fost deja salvat în siguranță} few {{formattedNumber} fișiere din acest album au fost deja salvate în siguranță} other {{formattedNumber} de fișiere din acest album au fost deja salvate în siguranță}}",
"filesBackedUpInAlbum": "{count, plural, one {Un fișier din acest album a fost deja salvat în siguranță} other {{formattedNumber} de fișiere din acest album au fost deja salvate în siguranță}}",
"@filesBackedUpInAlbum": {
"description": "Text to tell user how many files have been backed up in the album",
"placeholders": {
@@ -888,7 +888,7 @@
}
}
},
"filesBackedUpFromDevice": "{count, plural, one {Un fișier de pe acest dispozitiv a fost deja salvat în siguranță} few {{formattedNumber} fișiere de pe acest dispozitiv au fost deja salvate în siguranță} other {{formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță}}",
"filesBackedUpFromDevice": "{count, plural, one {Un fișier de pe acest dispozitiv a fost deja salvat în siguranță} other {{formattedNumber} de fișiere de pe acest dispozitiv fost deja salvate în siguranță}}",
"@filesBackedUpFromDevice": {
"description": "Text to tell user how many files have been backed up from this device",
"placeholders": {
@@ -1177,7 +1177,7 @@
"searchHint4": "Locație",
"searchHint5": "În curând: chipuri și căutare magică ✨",
"addYourPhotosNow": "Adăugați-vă fotografiile acum",
"searchResultCount": "{count, plural, one{{count} rezultat găsit} few {{count} rezultate găsite} other{{count} de rezultate găsite}}",
"searchResultCount": "{count, plural, one{{count} rezultat găsit} other{{count} de rezultate găsite}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1522,4 +1522,4 @@
"joinAlbumSubtext": "pentru a vedea și a adăuga fotografii",
"joinAlbumSubtextViewer": "pentru a adăuga la albumele distribuite",
"join": "Alăturare"
}
}

View File

@@ -477,7 +477,7 @@
}
},
"showMemories": "Показывать воспоминания",
"yearsAgo": "{count, plural, one{{count} год назад} few{{count} года назад} other{{count} лет назад}}",
"yearsAgo": "{count, plural, one{{count} год назад} other{{count} лет назад}}",
"backupSettings": "Настройки резервного копирования",
"backupStatus": "Статус резервного копирования",
"backupStatusDescription": "Элементы, сохранённые в резервной копии, появятся здесь",
@@ -1786,4 +1786,4 @@
"day": "День",
"filter": "Фильтр",
"font": "Шрифт"
}
}

View File

@@ -445,7 +445,7 @@
}
},
"showMemories": "Прикажи успомене",
"yearsAgo": "{count, plural, one{{count} година уназад} few {{count} године уназад} other{{count} година уназад}}",
"yearsAgo": "{count, plural, other{{count} година уназад}}",
"backupStatus": "Статус резервних копија",
"backupOverMobileData": "Копирај користећи мобилни интернет",
"backupVideos": "Копирај видео снимке",
@@ -496,7 +496,7 @@
}
}
},
"duplicateFileCountWithStorageSaved": "Обрисали сте {count, plural, one{{count} дупликат} few {{count} дупликата} other{{count} дупликата}}, ослобађам ({storageSaved}!)",
"duplicateFileCountWithStorageSaved": "Обрисали сте {count, plural, one{{count} дупликат} other{{count} дупликата}}, ослобађам ({storageSaved}!)",
"@duplicateFileCountWithStorageSaved": {
"description": "The text to display when the user has successfully cleaned up duplicate files",
"type": "text",
@@ -921,4 +921,4 @@
}
}
}
}
}

View File

@@ -459,7 +459,7 @@
"selectAll": "Markera allt",
"skip": "Hoppa över",
"updatingFolderSelection": "Uppdaterar mappval...",
"itemCount": "{count, plural, one{{count} objekt} other{{count} objekt}}",
"itemCount": "{count, plural, other{{count} objekt}}",
"deleteItemCount": "{count, plural, =1 {Radera {count} objekt} other {Radera {count} objekt}}",
"duplicateItemsGroup": "{count} filer, {formattedSize} vardera",
"@duplicateItemsGroup": {
@@ -477,7 +477,7 @@
}
},
"showMemories": "Visa minnen",
"yearsAgo": "{count, plural, one{{count} år sedan} other{{count} år sedan}}",
"yearsAgo": "{count, plural, other{{count} år sedan}}",
"backupSettings": "Säkerhetskopieringsinställningar",
"backupStatus": "Säkerhetskopieringsstatus",
"backupStatusDescription": "Objekt som har säkerhetskopierats kommer att visas här",
@@ -619,7 +619,7 @@
"viewAll": "Visa alla",
"inviteYourFriendsToEnte": "Bjud in dina vänner till Ente",
"fileTypes": "Filtyper",
"searchResultCount": "{count, plural, one{{count} resultat hittades} other{{count} resultat hittades}}",
"searchResultCount": "{count, plural, other{{count} resultat hittades}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -655,4 +655,4 @@
"newPerson": "Ny person",
"addName": "Lägg till namn",
"add": "Lägg till"
}
}

View File

@@ -372,7 +372,7 @@
"deleteFromBoth": "Her ikisinden de sil",
"newAlbum": "Yeni albüm",
"albums": "Albümler",
"memoryCount": "{count, plural, =0{hiç anı yok} one{{formattedCount} anı} other{{formattedCount} anı}}",
"memoryCount": "{count, plural, =0{hiç anı yok} other{{formattedCount} anı}}",
"@memoryCount": {
"description": "The text to display the number of memories",
"type": "text",
@@ -477,7 +477,7 @@
}
},
"showMemories": "Anıları göster",
"yearsAgo": "{count, plural, one{{count} yıl önce} other{{count} yıl önce}}",
"yearsAgo": "{count, plural, other{{count} yıl önce}}",
"backupSettings": "Yedekleme seçenekleri",
"backupStatus": "Yedekleme durumu",
"backupStatusDescription": "Eklenen öğeler burada görünecek",
@@ -1217,7 +1217,7 @@
"searchHint4": "Konum",
"searchHint5": "Çok yakında: Yüzler ve sihirli arama ✨",
"addYourPhotosNow": "Fotoğraflarınızı şimdi ekleyin",
"searchResultCount": "{count, plural, one{{count} yıl önce} other{{count} yıl önce}}",
"searchResultCount": "{count, plural, other{{count} yıl önce}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1777,4 +1777,4 @@
"different": "Farklı",
"sameperson": "Aynı kişi mi?",
"indexingPausedStatusDescription": "Dizin oluşturma duraklatıldı. Cihaz hazır olduğunda otomatik olarak devam edecektir. Cihaz, pil seviyesi, pil sağlığı ve termal durumu sağlıklı bir aralıkta olduğunda hazır kabul edilir."
}
}

View File

@@ -438,7 +438,7 @@
"selectAll": "Вибрати все",
"skip": "Пропустити",
"updatingFolderSelection": "Оновлення вибору теки...",
"itemCount": "{count, plural, one{{count} елемент} few {{count} елементи} many {{count} елементів} other{{count} елементів}}",
"itemCount": "{count, plural, one{{count} елемент} other{{count} елементів}}",
"deleteItemCount": "{count, plural, =1 {Видалено {count} елемент} other {Видалено {count} елементів}}",
"duplicateItemsGroup": "{count} файлів, кожен по {formattedSize}",
"@duplicateItemsGroup": {
@@ -1172,7 +1172,7 @@
"searchHint4": "Розташування",
"searchHint5": "Незабаром: Обличчя і магічний пошук ✨",
"addYourPhotosNow": "Додайте свої фотографії",
"searchResultCount": "{count, plural, one{Знайдено {count} результат} few {Знайдено {count} результати} many {Знайдено {count} результатів} other{Знайдено {count} результати}}",
"searchResultCount": "{count, plural, one{Знайдено {count} результат} other{Знайдено {count} результати}}",
"@searchResultCount": {
"description": "Text to tell user how many results were found for their search query",
"placeholders": {
@@ -1510,4 +1510,4 @@
"legacyInvite": "{email} запросив вас стати довіреною особою",
"authToManageLegacy": "Авторизуйтесь, щоби керувати довіреними контактами",
"useDifferentPlayerInfo": "Виникли проблеми з відтворенням цього відео? Натисніть і утримуйте тут, щоб спробувати інший плеєр."
}
}

View File

@@ -459,7 +459,7 @@
"selectAll": "全选",
"skip": "跳过",
"updatingFolderSelection": "正在更新文件夹选择...",
"itemCount": "{count, plural, one{{count} 个项目} other{{count} 个项目}}",
"itemCount": "{count, plural, other{{count} 个项目}}",
"deleteItemCount": "{count, plural, =1 {删除 {count} 个项目} other {删除 {count} 个项目}}",
"duplicateItemsGroup": "{count} 个文件,每个文件 {formattedSize}",
"@duplicateItemsGroup": {
@@ -477,7 +477,7 @@
}
},
"showMemories": "显示回忆",
"yearsAgo": "{count, plural, one{{count} 年前} other{{count} 年前}}",
"yearsAgo": "{count, plural, other{{count} 年前}}",
"backupSettings": "备份设置",
"backupStatus": "备份状态",
"backupStatusDescription": "已备份的项目将显示在此处",
@@ -1934,4 +1934,4 @@
"related": "相关",
"hoorayyyy": "耶~~!",
"nothingToTidyUpHere": "这里没什么可清理的"
}
}

View File

@@ -0,0 +1,3 @@
import "dart:developer";
var devLog = log;

View File

@@ -25,12 +25,14 @@ import "package:photos/db/ml/db.dart";
import 'package:photos/ente_theme_data.dart';
import "package:photos/extensions/stop_watch.dart";
import "package:photos/l10n/l10n.dart";
import 'package:photos/module/upload/service/file_uploader.dart';
import "package:photos/service_locator.dart";
import "package:photos/services/account/user_service.dart";
import 'package:photos/services/app_lifecycle_service.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/favorites_service.dart';
import 'package:photos/services/home_widget_service.dart';
import "package:photos/services/local/import/local_import.dart";
import 'package:photos/services/local_file_update_service.dart';
import "package:photos/services/machine_learning/face_ml/person/person_service.dart";
import 'package:photos/services/machine_learning/ml_service.dart';
@@ -38,7 +40,6 @@ import 'package:photos/services/machine_learning/semantic_search/semantic_search
import "package:photos/services/notification_service.dart";
import 'package:photos/services/push_service.dart';
import 'package:photos/services/search_service.dart';
import 'package:photos/services/sync/local_sync_service.dart';
import 'package:photos/services/sync/remote_sync_service.dart';
import "package:photos/services/sync/sync_service.dart";
import "package:photos/services/video_preview_service.dart";
@@ -47,7 +48,6 @@ import "package:photos/src/rust/frb_generated.dart";
import 'package:photos/ui/tools/app_lock.dart';
import 'package:photos/ui/tools/lock_screen.dart';
import "package:photos/utils/email_util.dart";
import 'package:photos/utils/file_uploader.dart';
import "package:photos/utils/lock_screen_settings.dart";
import 'package:shared_preferences/shared_preferences.dart';
@@ -167,7 +167,7 @@ Future<void> _runMinimally(String taskId, TimeLogger tlog) async {
// Upload & Sync Related
await FileUploader.instance.init(prefs, true);
LocalFileUpdateService.instance.init(prefs);
await LocalSyncService.instance.init(prefs);
await LocalImportService.instance.init(prefs);
RemoteSyncService.instance.init(prefs);
await SyncService.instance.init(prefs);
@@ -258,7 +258,7 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
_logger.info("FileUploader init done $tlog");
_logger.info("LocalSyncService init $tlog");
await LocalSyncService.instance.init(preferences);
await LocalImportService.instance.init(preferences);
_logger.info("LocalSyncService init done $tlog");
RemoteSyncService.instance.init(preferences);
@@ -349,9 +349,9 @@ Future runWithLogs(Function() function, {String prefix = ""}) async {
body: function,
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
maxLogFiles: 5,
sentryDsn: kDebugMode ? sentryDebugDSN : sentryDSN,
sentryDsn: kDebugMode ? null : sentryDSN,
tunnel: sentryTunnel,
enableInDebugMode: true,
enableInDebugMode: !kDebugMode, // todo: rewrite neeraj revert this
prefix: prefix,
),
);

View File

@@ -1,66 +0,0 @@
import 'dart:convert';
class CollectionFileItem {
final int id;
final String encryptedKey;
final String keyDecryptionNonce;
CollectionFileItem(
this.id,
this.encryptedKey,
this.keyDecryptionNonce,
);
CollectionFileItem copyWith({
int? id,
String? encryptedKey,
String? keyDecryptionNonce,
}) {
return CollectionFileItem(
id ?? this.id,
encryptedKey ?? this.encryptedKey,
keyDecryptionNonce ?? this.keyDecryptionNonce,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'encryptedKey': encryptedKey,
'keyDecryptionNonce': keyDecryptionNonce,
};
}
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
return CollectionFileItem(
map['id'],
map['encryptedKey'],
map['keyDecryptionNonce'],
);
}
String toJson() => json.encode(toMap());
factory CollectionFileItem.fromJson(String source) =>
CollectionFileItem.fromMap(json.decode(source));
@override
String toString() =>
'CollectionFileItem(id: $id, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce)';
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is CollectionFileItem &&
o.id == id &&
o.encryptedKey == encryptedKey &&
o.keyDecryptionNonce == keyDecryptionNonce;
}
@override
int get hashCode =>
id.hashCode ^ encryptedKey.hashCode ^ keyDecryptionNonce.hashCode;
}

View File

@@ -0,0 +1,35 @@
import "dart:typed_data";
import "package:ente_crypto/ente_crypto.dart";
class CollectionFileRequest {
final int id;
final String encryptedKey;
final String keyDecryptionNonce;
CollectionFileRequest(
this.id,
this.encryptedKey,
this.keyDecryptionNonce,
);
static Map<String, dynamic> req(
int id, {
required Uint8List encKey,
required Uint8List encKeyNonce,
}) {
return {
'id': id,
'encryptedKey': CryptoUtil.bin2base64(encKey),
'keyDecryptionNonce': CryptoUtil.bin2base64(encKeyNonce),
};
}
Map<String, dynamic> toMap() {
return {
'id': id,
'encryptedKey': encryptedKey,
'keyDecryptionNonce': keyDecryptionNonce,
};
}
}

View File

@@ -0,0 +1,197 @@
import "dart:convert";
import "dart:typed_data";
import "package:photos/models/api/diff/trash_time.dart";
import "package:photos/models/file/remote/asset.dart";
class Info {
final int fileSize;
final int thumbSize;
static Info? fromJson(Map<String, dynamic>? json) {
if (json == null) return null;
return Info(
fileSize: json['fileSize'] ?? -1,
thumbSize: json['thumbSize'] ?? -1,
);
}
Info({required this.fileSize, required this.thumbSize});
Map<String, dynamic> toJson() {
return {
'fileSize': fileSize,
'thumbSize': thumbSize,
};
}
String toEncodedJson() {
return jsonEncode(toJson());
}
static Info? fromEncodedJson(String? encodedJson) {
if (encodedJson == null) return null;
return Info.fromJson(jsonDecode(encodedJson));
}
}
class Metadata {
final Map<String, dynamic> data;
final int version;
Metadata({required this.data, required this.version});
static fromJson(Map<String, dynamic> json) {
if (json.isEmpty || json['data'] == null) return null;
return Metadata(
data: json['data'],
version: json['version'],
);
}
Map<String, dynamic> toJson() {
return {
'data': data,
'version': version,
};
}
static Metadata? fromEncodedJson(String? encodedJson) {
if (encodedJson == null) return null;
return Metadata.fromJson(jsonDecode(encodedJson));
}
String toEncodedJson() {
return jsonEncode(toJson());
}
}
class ApiFileItem {
final int fileID;
final int ownerID;
final Uint8List? thumnailDecryptionHeader;
final Uint8List? fileDecryptionHeader;
final Metadata? metadata;
final Metadata? privMagicMetadata;
final Metadata? pubMagicMetadata;
final Info? info;
ApiFileItem({
required this.fileID,
required this.ownerID,
this.thumnailDecryptionHeader,
this.fileDecryptionHeader,
this.metadata,
this.privMagicMetadata,
this.pubMagicMetadata,
this.info,
});
factory ApiFileItem.deleted(int fileID, int ownerID) {
return ApiFileItem(
fileID: fileID,
ownerID: ownerID,
);
}
List<Object?> filesMetadataRowValues() {
return [
fileID,
metadata?.toEncodedJson(),
privMagicMetadata?.toEncodedJson(),
pubMagicMetadata?.toEncodedJson(),
info?.toEncodedJson(),
];
}
RemoteAsset toRemoteAsset() {
return RemoteAsset.fromMetadata(
id: fileID,
ownerID: ownerID,
thumbHeader: thumnailDecryptionHeader!,
fileHeader: fileDecryptionHeader!,
metadata: metadata!,
privateMetadata: privMagicMetadata,
publicMetadata: pubMagicMetadata,
info: info,
);
}
String get title =>
pubMagicMetadata?.data['editedName'] ?? metadata?.data['title'] ?? "";
String get nonEditedTitle {
return metadata?.data['title'] ?? "";
}
String? get localID => metadata?.data['localID'];
String? get deviceFolder => metadata?.data['deviceFolder'];
int get creationTime =>
pubMagicMetadata?.data['editedTime'] ??
metadata?.data['creationTime'] ??
0;
int get modificationTime =>
metadata?.data['modificationTime'] ?? creationTime;
// note: during remote to local sync, older live photo hash format from desktop
// is already converted to the new format
String? get hash => metadata?.data['hash'];
int get fileSize => info?.fileSize ?? -1;
}
class DiffItem {
final int collectionID;
final bool isDeleted;
final Uint8List? encFileKey;
final Uint8List? encFileKeyNonce;
final int updatedAt;
final int? createdAt;
final ApiFileItem fileItem;
final TrashTime? trashTime;
DiffItem({
required this.collectionID,
required this.isDeleted,
required this.updatedAt,
required this.fileItem,
this.createdAt,
this.encFileKey,
this.encFileKeyNonce,
this.trashTime,
});
int get fileID => fileItem.fileID;
List<Object?> collectionFileRowValues() {
return [
collectionID,
fileID,
encFileKey,
encFileKeyNonce,
createdAt,
updatedAt,
];
}
List<Object?> trashRowValues() {
return [
fileID,
fileItem.ownerID,
collectionID,
encFileKey,
encFileKeyNonce,
fileItem.fileDecryptionHeader,
fileItem.thumnailDecryptionHeader,
fileItem.metadata?.toEncodedJson(),
fileItem.privMagicMetadata?.toEncodedJson(),
fileItem.pubMagicMetadata?.toEncodedJson(),
fileItem.info?.toEncodedJson(),
trashTime!.createdAt,
trashTime!.updatedAt,
trashTime!.deleteBy,
];
}
}

View File

@@ -0,0 +1,21 @@
class TrashTime {
int createdAt;
int updatedAt;
int deleteBy;
TrashTime({
required this.createdAt,
required this.updatedAt,
required this.deleteBy,
});
TrashTime.fromMap(Map<String, dynamic> map)
: createdAt = map["createdAt"] as int,
updatedAt = map["updatedAt"] as int,
deleteBy = map["deleteBy"] as int;
Map<String, dynamic> toMap() {
return {
"createdAt": createdAt,
"updatedAt": updatedAt,
"deleteBy": deleteBy,
};
}
}

View File

@@ -1,3 +1,4 @@
import "dart:convert";
import 'dart:core';
import 'package:flutter/foundation.dart';
@@ -5,6 +6,7 @@ import "package:photos/core/configuration.dart";
import "package:photos/extensions/user_extension.dart";
import "package:photos/models/api/collection/public_url.dart";
import "package:photos/models/api/collection/user.dart";
import "package:photos/models/collection/collection_old.dart";
import "package:photos/models/metadata/collection_magic.dart";
import "package:photos/models/metadata/common_keys.dart";
@@ -12,33 +14,18 @@ class Collection {
final int id;
final User owner;
final String encryptedKey;
final String? keyDecryptionNonce;
@Deprecated("Use collectionName instead")
// keyDecryptionNonce will be empty string for collections shared with the user
final String keyDecryptionNonce;
String? name;
// encryptedName & nameDecryptionNonce will be null for collections
// created before we started encrypting collection name
final String? encryptedName;
final String? nameDecryptionNonce;
final CollectionType type;
final CollectionAttributes attributes;
final List<User> sharees;
final List<PublicURL> publicURLs;
final int updationTime;
final bool isDeleted;
// In early days before public launch, we used to store collection name
// un-encrypted. decryptName will be value either decrypted value for
// encryptedName or name itself.
String? decryptedName;
// decryptedPath will be null for collections now owned by user, deleted
// collections, && collections which don't have a path. The path is used
// to map local on-device album on mobile to remote collection on ente.
String? decryptedPath;
String? mMdEncodedJson;
String? mMdPubEncodedJson;
String? sharedMmdJson;
final String? localPath;
String mMdEncodedJson;
String mMdPubEncodedJson;
String sharedMmdJson;
int mMdVersion = 0;
int mMbPubVersion = 0;
int sharedMmdVersion = 0;
@@ -47,14 +34,13 @@ class Collection {
ShareeMagicMetadata? _sharedMmd;
CollectionMagicMetadata get magicMetadata =>
_mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
_mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson);
CollectionPubMagicMetadata get pubMagicMetadata =>
_pubMmd ??
CollectionPubMagicMetadata.fromEncodedJson(mMdPubEncodedJson ?? '{}');
_pubMmd ?? CollectionPubMagicMetadata.fromEncodedJson(mMdPubEncodedJson);
ShareeMagicMetadata get sharedMagicMetadata =>
_sharedMmd ?? ShareeMagicMetadata.fromEncodedJson(sharedMmdJson ?? '{}');
_sharedMmd ?? ShareeMagicMetadata.fromEncodedJson(sharedMmdJson);
set magicMetadata(CollectionMagicMetadata? val) => _mmd = val;
@@ -69,32 +55,58 @@ class Collection {
!isOwner(Configuration.instance.getUserID() ?? -1)) {
return '${owner.nameOrEmail}\'s favorites';
}
return decryptedName ?? name ?? "Unnamed Album";
return name ?? "Unnamed Album";
}
// set the value for both name and decryptedName till we finish migration
void setName(String newName) {
// ignore: deprecated_member_use_from_same_package
name = newName;
decryptedName = newName;
}
Collection(
this.id,
this.owner,
this.encryptedKey,
this.keyDecryptionNonce,
this.name,
this.encryptedName,
this.nameDecryptionNonce,
this.type,
this.attributes,
this.sharees,
this.publicURLs,
this.updationTime, {
Collection({
required this.id,
required this.owner,
required this.encryptedKey,
required this.keyDecryptionNonce,
required this.name,
required this.type,
required this.sharees,
required this.publicURLs,
required this.updationTime,
required this.localPath,
this.isDeleted = false,
this.mMdEncodedJson = '{}',
this.mMdPubEncodedJson = '{}',
this.sharedMmdJson = '{}',
this.mMdVersion = 0,
this.mMbPubVersion = 0,
this.sharedMmdVersion = 0,
});
factory Collection.fromOldCollection(CollectionV2 collection) {
return Collection(
id: collection.id,
owner: collection.owner,
encryptedKey: collection.encryptedKey,
// note: keyDecryptionNonce will be null in case of collections
// shared with the user
keyDecryptionNonce: collection.keyDecryptionNonce ?? '',
name: collection.displayName,
type: collection.type,
sharees: collection.sharees,
publicURLs: collection.publicURLs,
updationTime: collection.updationTime,
localPath: collection.decryptedPath,
isDeleted: collection.isDeleted,
mMbPubVersion: collection.mMbPubVersion,
mMdPubEncodedJson: collection.mMdPubEncodedJson ?? '{}',
mMdVersion: collection.mMdVersion,
mMdEncodedJson: collection.mMdEncodedJson ?? '{}',
sharedMmdJson: collection.sharedMmdJson ?? '{}',
sharedMmdVersion: collection.sharedMmdVersion,
);
}
bool isArchived() {
return mMdVersion > 0 && magicMetadata.visibility == archiveVisibility;
}
@@ -122,6 +134,15 @@ class Collection {
return mMdVersion > 0 && (magicMetadata.visibility == hiddenVisibility);
}
int get visibility {
if (isHidden()) {
return hiddenVisibility;
} else if (isArchived() || hasShareeArchived()) {
return archiveVisibility;
}
return 0;
}
bool isDefaultHidden() {
return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
}
@@ -191,7 +212,10 @@ class Collection {
// device album based on path. The path is nothing but the name of the device
// album.
bool canLinkToDevicePath(int userID) {
return isOwner(userID) && !isDeleted && attributes.encryptedPath != null;
return isOwner(userID) &&
!isDeleted &&
localPath != null &&
localPath != '';
}
void updateSharees(List<User> newSharees) {
@@ -205,72 +229,90 @@ class Collection {
String? encryptedKey,
String? keyDecryptionNonce,
String? name,
String? encryptedName,
String? nameDecryptionNonce,
CollectionType? type,
CollectionAttributes? attributes,
List<User>? sharees,
List<PublicURL>? publicURLs,
int? updationTime,
bool? isDeleted,
String? localPath,
String? mMdEncodedJson,
int? mMdVersion,
String? decryptedName,
String? decryptedPath,
String? mMdPubEncodedJson,
int? mMbPubVersion,
String? sharedMmdJson,
int? sharedMmdVersion,
}) {
final Collection result = Collection(
id ?? this.id,
owner ?? this.owner,
encryptedKey ?? this.encryptedKey,
keyDecryptionNonce ?? this.keyDecryptionNonce,
// ignore: deprecated_member_use_from_same_package
name ?? this.name,
encryptedName ?? this.encryptedName,
nameDecryptionNonce ?? this.nameDecryptionNonce,
type ?? this.type,
attributes ?? this.attributes,
sharees ?? this.sharees,
publicURLs ?? this.publicURLs,
updationTime ?? this.updationTime,
id: id ?? this.id,
owner: owner ?? this.owner,
encryptedKey: encryptedKey ?? this.encryptedKey,
keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
name: name ?? this.name,
type: type ?? this.type,
sharees: sharees ?? this.sharees,
publicURLs: publicURLs ?? this.publicURLs,
updationTime: updationTime ?? this.updationTime,
localPath: localPath ?? this.localPath,
isDeleted: isDeleted ?? this.isDeleted,
mMdEncodedJson: mMdEncodedJson ?? this.mMdEncodedJson,
mMdVersion: mMdVersion ?? this.mMdVersion,
mMdPubEncodedJson: mMdPubEncodedJson ?? this.mMdPubEncodedJson,
mMbPubVersion: mMbPubVersion ?? this.mMbPubVersion,
sharedMmdJson: sharedMmdJson ?? this.sharedMmdJson,
sharedMmdVersion: sharedMmdVersion ?? this.sharedMmdVersion,
);
result.mMdVersion = mMdVersion ?? this.mMdVersion;
result.mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson;
result.decryptedName = decryptedName ?? this.decryptedName;
result.decryptedPath = decryptedPath ?? this.decryptedPath;
result.mMbPubVersion = mMbPubVersion;
result.mMdPubEncodedJson = mMdPubEncodedJson;
result.sharedMmdVersion = sharedMmdVersion;
result.sharedMmdJson = sharedMmdJson;
return result;
}
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
final sharees = (map['sharees'] == null || map['sharees'].length == 0)
? <User>[]
: List<User>.from(map['sharees'].map((x) => User.fromMap(x)));
final publicURLs =
(map['publicURLs'] == null || map['publicURLs'].length == 0)
? <PublicURL>[]
: List<PublicURL>.from(
map['publicURLs'].map((x) => PublicURL.fromMap(x)),
);
return Collection(
map['id'],
User.fromMap(map['owner']),
map['encryptedKey'],
map['keyDecryptionNonce'],
map['name'],
map['encryptedName'],
map['nameDecryptionNonce'],
typeFromString(map['type']),
CollectionAttributes.fromMap(map['attributes']),
sharees,
publicURLs,
map['updationTime'],
isDeleted: map['isDeleted'] ?? false,
static Collection fromRow(Map<String, dynamic> map) {
final sharees = List<User>.from(
(json.decode(map['sharees']) as List).map((x) => User.fromMap(x)),
);
final List<PublicURL> publicURLs = List<PublicURL>.from(
(json.decode(map['public_urls']) as List)
.map((x) => PublicURL.fromMap(x)),
);
return Collection(
id: map['id'],
owner: User.fromJson(map['owner']),
encryptedKey: map['enc_key'],
keyDecryptionNonce: map['enc_key_nonce'],
name: map['name'],
type: typeFromString(map['type']),
sharees: sharees,
publicURLs: publicURLs,
updationTime: map['updation_time'],
localPath: map['local_path'],
isDeleted: (map['is_deleted'] as int) == 1,
mMdEncodedJson: map['mmd_encoded_json'],
mMdVersion: map['mmd_ver'],
mMdPubEncodedJson: map['pub_mmd_encoded_json'],
mMbPubVersion: map['pub_mmd_ver'],
sharedMmdJson: map['shared_mmd_json'],
sharedMmdVersion: map['shared_mmd_ver'],
);
}
List<Object?> rowValiues() {
return [
id,
owner.toJson(),
encryptedKey,
keyDecryptionNonce,
name,
typeToString(type),
localPath,
isDeleted ? 1 : 0,
updationTime,
json.encode(sharees.map((x) => x.toMap()).toList()),
json.encode(publicURLs.map((x) => x.toMap()).toList()),
mMdEncodedJson,
mMdVersion,
mMdPubEncodedJson,
mMbPubVersion,
sharedMmdJson,
sharedMmdVersion,
];
}
}

View File

@@ -0,0 +1,252 @@
import "package:photos/models/api/collection/public_url.dart";
import "package:photos/models/api/collection/user.dart";
import "package:photos/models/collection/collection.dart";
import "package:photos/models/metadata/collection_magic.dart";
import "package:photos/models/metadata/common_keys.dart";
class CollectionV2 {
final int id;
final User owner;
final String encryptedKey;
final String? keyDecryptionNonce;
@Deprecated("Use collectionName instead")
String? name;
// encryptedName & nameDecryptionNonce will be null for collections
// created before we started encrypting collection name
final String? encryptedName;
final String? nameDecryptionNonce;
final CollectionType type;
final CollectionAttributes attributes;
final List<User> sharees;
final List<PublicURL> publicURLs;
final int updationTime;
final bool isDeleted;
// In early days before public launch, we used to store collection name
// un-encrypted. decryptName will be value either decrypted value for
// encryptedName or name itself.
String? decryptedName;
// decryptedPath will be null for collections now owned by user, deleted
// collections, && collections which don't have a path. The path is used
// to map local on-device album on mobile to remote collection on ente.
String? decryptedPath;
String? mMdEncodedJson;
String? mMdPubEncodedJson;
String? sharedMmdJson;
int mMdVersion = 0;
int mMbPubVersion = 0;
int sharedMmdVersion = 0;
CollectionMagicMetadata? _mmd;
CollectionPubMagicMetadata? _pubMmd;
ShareeMagicMetadata? _sharedMmd;
CollectionMagicMetadata get magicMetadata =>
_mmd ?? CollectionMagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
CollectionPubMagicMetadata get pubMagicMetadata =>
_pubMmd ??
CollectionPubMagicMetadata.fromEncodedJson(mMdPubEncodedJson ?? '{}');
ShareeMagicMetadata get sharedMagicMetadata =>
_sharedMmd ?? ShareeMagicMetadata.fromEncodedJson(sharedMmdJson ?? '{}');
set magicMetadata(CollectionMagicMetadata? val) => _mmd = val;
set pubMagicMetadata(CollectionPubMagicMetadata? val) => _pubMmd = val;
set sharedMagicMetadata(ShareeMagicMetadata? val) => _sharedMmd = val;
String get displayName => decryptedName ?? name ?? "Unnamed Album";
// set the value for both name and decryptedName till we finish migration
void setName(String newName) {
name = newName;
decryptedName = newName;
}
CollectionV2(
this.id,
this.owner,
this.encryptedKey,
this.keyDecryptionNonce,
this.name,
this.encryptedName,
this.nameDecryptionNonce,
this.type,
this.attributes,
this.sharees,
this.publicURLs,
this.updationTime, {
this.isDeleted = false,
});
bool isArchived() {
return mMdVersion > 0 && magicMetadata.visibility == archiveVisibility;
}
bool hasShareeArchived() {
return sharedMmdVersion > 0 &&
sharedMagicMetadata.visibility == archiveVisibility;
}
// hasLink returns true if there's any link attached to the collection
// including expired links
bool get hasLink => publicURLs.isNotEmpty;
bool get hasCover => (pubMagicMetadata.coverID ?? 0) > 0;
// hasSharees returns true if the collection is shared with other ente users
bool get hasSharees => sharees.isNotEmpty;
bool get isPinned => (magicMetadata.order ?? 0) != 0;
bool isHidden() {
if (isDefaultHidden()) {
return true;
}
return mMdVersion > 0 && (magicMetadata.visibility == hiddenVisibility);
}
bool isDefaultHidden() {
return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
}
bool isQuickLinkCollection() {
return (magicMetadata.subType ?? 0) == subTypeSharedFilesCollection &&
!hasSharees;
}
List<User> getSharees() {
return sharees;
}
bool isOwner(int userID) {
return (owner.id ?? -100) == userID;
}
bool isDownloadEnabledForPublicLink() {
if (publicURLs.isEmpty) {
return false;
}
return publicURLs.first.enableDownload;
}
bool isCollectEnabledForPublicLink() {
if (publicURLs.isEmpty) {
return false;
}
return publicURLs.first.enableCollect;
}
bool get isJoinEnabled {
if (publicURLs.isEmpty) {
return false;
}
return publicURLs.first.enableJoin;
}
CollectionParticipantRole getRole(int userID) {
if (isOwner(userID)) {
return CollectionParticipantRole.owner;
}
if (sharees.isEmpty) {
return CollectionParticipantRole.unknown;
}
for (final User u in sharees) {
if (u.id == userID) {
if (u.isViewer) {
return CollectionParticipantRole.viewer;
} else if (u.isCollaborator) {
return CollectionParticipantRole.collaborator;
}
}
}
return CollectionParticipantRole.unknown;
}
// canLinkToDevicePath returns true if the collection can be linked to local
// device album based on path. The path is nothing but the name of the device
// album.
bool canLinkToDevicePath(int userID) {
return isOwner(userID) && !isDeleted && attributes.encryptedPath != null;
}
void updateSharees(List<User> newSharees) {
sharees.clear();
sharees.addAll(newSharees);
}
CollectionV2 copyWith({
int? id,
User? owner,
String? encryptedKey,
String? keyDecryptionNonce,
String? name,
String? encryptedName,
String? nameDecryptionNonce,
CollectionType? type,
CollectionAttributes? attributes,
List<User>? sharees,
List<PublicURL>? publicURLs,
int? updationTime,
bool? isDeleted,
String? mMdEncodedJson,
int? mMdVersion,
String? decryptedName,
String? decryptedPath,
}) {
final CollectionV2 result = CollectionV2(
id ?? this.id,
owner ?? this.owner,
encryptedKey ?? this.encryptedKey,
keyDecryptionNonce ?? this.keyDecryptionNonce,
name ?? this.name,
encryptedName ?? this.encryptedName,
nameDecryptionNonce ?? this.nameDecryptionNonce,
type ?? this.type,
attributes ?? this.attributes,
sharees ?? this.sharees,
publicURLs ?? this.publicURLs,
updationTime ?? this.updationTime,
isDeleted: isDeleted ?? this.isDeleted,
);
result.mMdVersion = mMdVersion ?? this.mMdVersion;
result.mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson;
result.decryptedName = decryptedName ?? this.decryptedName;
result.decryptedPath = decryptedPath ?? this.decryptedPath;
result.mMbPubVersion = mMbPubVersion;
result.mMdPubEncodedJson = mMdPubEncodedJson;
result.sharedMmdVersion = sharedMmdVersion;
result.sharedMmdJson = sharedMmdJson;
return result;
}
static CollectionV2 fromMap(Map<String, dynamic> map) {
final sharees = (map['sharees'] == null || map['sharees'].length == 0)
? <User>[]
: List<User>.from(map['sharees'].map((x) => User.fromMap(x)));
final publicURLs =
(map['publicURLs'] == null || map['publicURLs'].length == 0)
? <PublicURL>[]
: List<PublicURL>.from(
map['publicURLs'].map((x) => PublicURL.fromMap(x)),
);
return CollectionV2(
map['id'],
User.fromMap(map['owner']),
map['encryptedKey'],
map['keyDecryptionNonce'],
map['name'],
map['encryptedName'],
map['nameDecryptionNonce'],
typeFromString(map['type']),
CollectionAttributes.fromMap(map['attributes']),
sharees,
publicURLs,
map['updationTime'],
isDeleted: map['isDeleted'] ?? false,
);
}
}

View File

@@ -1,13 +1,12 @@
import "package:photo_manager/photo_manager.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/upload_strategy.dart';
class DeviceCollection {
final String id;
final String name;
AssetPathEntity assetPathEntity;
final int count;
final bool shouldBackup;
UploadStrategy uploadStrategy;
final String? coverId;
int? collectionID;
EnteFile? thumbnail;
@@ -15,10 +14,16 @@ class DeviceCollection {
return collectionID != null && collectionID! != -1;
}
String get name {
return assetPathEntity.name;
}
String get id {
return assetPathEntity.id;
}
DeviceCollection(
this.id,
this.name, {
this.coverId,
this.assetPathEntity, {
this.count = 0,
this.collectionID,
this.thumbnail,

View File

@@ -85,10 +85,10 @@ class DuplicateFiles {
sortByCollectionName() {
files.sort((first, second) {
final firstName = collectionsService
.getCollectionByID(first.collectionID!)!
.getCollectionByID(first.cf!.collectionID)!
.displayName;
final secondName = collectionsService
.getCollectionByID(second.collectionID!)!
.getCollectionByID(second.cf!.collectionID!)!
.displayName;
return firstName.compareTo(secondName);
});

View File

@@ -1,13 +1,13 @@
import "package:photos/core/configuration.dart";
import 'package:photos/models/file/extensions/r_asset_props.dart';
import "package:photos/models/file/file.dart";
import "package:photos/models/file/file_type.dart";
import "package:photos/models/file/trash_file.dart";
import "package:photos/services/collections_service.dart";
extension FilePropsExtn on EnteFile {
bool get isLivePhoto => fileType == FileType.livePhoto;
bool get isMotionPhoto => (pubMagicMetadata?.mvi ?? 0) > 0;
bool get isMotionPhoto => rAsset?.isMotionPhoto ?? false;
bool get isLiveOrMotionPhoto => isLivePhoto || isMotionPhoto;
@@ -23,8 +23,8 @@ extension FilePropsExtn on EnteFile {
if (fileType != FileType.image) {
return false;
}
if (pubMagicMetadata?.mediaType != null) {
return (pubMagicMetadata!.mediaType! & 1) == 1;
if (rAsset?.mediaType != null) {
return (rAsset!.mediaType! & 1) == 1;
}
return null;
}
@@ -42,22 +42,21 @@ extension FilePropsExtn on EnteFile {
bool get canEditMetaInfo => isUploaded && isOwner;
bool get isTrash => this is TrashFile;
bool get isTrash => trashTime != null;
// Return true if the file was uploaded via collect photos workflow
bool get isCollect => uploaderName != null;
String? get uploaderName => pubMagicMetadata?.uploaderName;
String? get uploaderName => rAsset?.uploaderName;
bool get skipIndex => !isUploaded || fileType == FileType.other;
bool canReUpload(int userID) =>
localID != null &&
localID!.isNotEmpty &&
lAsset != null &&
cf != null &&
isOwner &&
collectionID != null &&
(CollectionsService.instance
.getCollectionByID(collectionID!)
.getCollectionByID(cf!.collectionID)
?.isOwner(userID) ??
false);
}

View File

@@ -0,0 +1,7 @@
import "package:photos/models/file/remote/asset.dart";
extension RemoteAssetExtension on RemoteAsset {
bool get isMotionPhoto {
return (motionVideoIndex ?? 0) > 0;
}
}

View File

@@ -1,125 +1,102 @@
import 'dart:io';
import "dart:core";
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/constants.dart';
import "package:photos/models/api/diff/trash_time.dart";
import 'package:photos/models/file/file_type.dart';
import "package:photos/models/file/remote/asset.dart";
import "package:photos/models/file/remote/collection_file.dart";
import "package:photos/models/local/shared_asset.dart";
import 'package:photos/models/location/location.dart';
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/module/download/file_url.dart";
import 'package:photos/utils/exif_util.dart';
import 'package:photos/utils/file_uploader_util.dart';
import "package:photos/utils/panorama_util.dart";
import 'package:photos/utils/standalone/date_time.dart';
import "package:photos/services/local/asset_entity.service.dart";
//Todo: files with no location data have lat and long set to 0.0. This should ideally be null.
class EnteFile {
static final _logger = Logger('EnteFile');
AssetEntity? lAsset;
RemoteAsset? rAsset;
CollectionFile? cf;
TrashTime? trashTime;
SharedAsset? sharedAsset;
int? generatedID;
int? uploadedFileID;
int? ownerID;
int? collectionID;
String? localID;
String? title;
String? deviceFolder;
int? creationTime;
int? modificationTime;
int? updationTime;
int? addedTime;
Location? location;
late Location? location;
late FileType fileType;
int? fileSubType;
int? duration;
String? exif;
String? hash;
int? metadataVersion;
String? encryptedKey;
String? keyDecryptionNonce;
String? fileDecryptionHeader;
String? thumbnailDecryptionHeader;
String? metadataDecryptionHeader;
int? fileSize;
String? mMdEncodedJson;
int mMdVersion = 0;
MagicMetadata? _mmd;
MagicMetadata get magicMetadata =>
_mmd ?? MagicMetadata.fromEncodedJson(mMdEncodedJson ?? '{}');
set magicMetadata(val) => _mmd = val;
// public magic metadata is shared if during file/album sharing
String? pubMmdEncodedJson;
int pubMmdVersion = 0;
PubMagicMetadata? _pubMmd;
PubMagicMetadata? get pubMagicMetadata =>
_pubMmd ?? PubMagicMetadata.fromEncodedJson(pubMmdEncodedJson ?? '{}');
set pubMagicMetadata(val) => _pubMmd = val;
// in Version 1, live photo hash is stored as zip's hash.
// in V2: LivePhoto hash is stored as imgHash:vidHash
static const kCurrentMetadataVersion = 2;
static final _logger = Logger('File');
EnteFile();
static Future<EnteFile> fromAsset(String pathName, AssetEntity asset) async {
static Future<EnteFile> fromAsset(String pathName, AssetEntity lAsset) async {
final EnteFile file = EnteFile();
file.localID = asset.id;
file.title = asset.title;
file.lAsset = lAsset;
file.deviceFolder = pathName;
file.location =
Location(latitude: asset.latitude, longitude: asset.longitude);
file.fileType = fileTypeFromAsset(asset);
file.creationTime = parseFileCreationTime(file.title, asset);
file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
file.fileSubType = asset.subtype;
file.metadataVersion = kCurrentMetadataVersion;
Location(latitude: lAsset.latitude, longitude: lAsset.longitude);
file.fileType = enteTypeFromAsset(lAsset);
file.creationTime = AssetEntityService.estimateCreationTime(lAsset);
file.modificationTime = lAsset.modifiedDateTime.microsecondsSinceEpoch;
return file;
}
static int parseFileCreationTime(String? fileTitle, AssetEntity asset) {
int creationTime = asset.createDateTime.microsecondsSinceEpoch;
final int modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
if (creationTime >= jan011981Time) {
// assuming that fileSystem is returning correct creationTime.
// During upload, this might get overridden with exif Creation time
// When the assetModifiedTime is less than creationTime, than just use
// that as creationTime. This is to handle cases where file might be
// copied to the fileSystem from somewhere else See #https://superuser.com/a/1091147
if (modificationTime >= jan011981Time &&
modificationTime < creationTime) {
_logger.info(
'LocalID: ${asset.id} modification time is less than creation time. Using modification time as creation time',
);
creationTime = modificationTime;
}
return creationTime;
} else {
if (modificationTime >= jan011981Time) {
creationTime = modificationTime;
} else {
creationTime = DateTime.now().toUtc().microsecondsSinceEpoch;
}
try {
final parsedDateTime = parseDateTimeFromFileNameV2(
basenameWithoutExtension(fileTitle ?? ""),
);
if (parsedDateTime != null) {
creationTime = parsedDateTime.microsecondsSinceEpoch;
}
} catch (e) {
// ignore
}
}
return creationTime;
static EnteFile fromAssetSync(AssetEntity asset) {
final EnteFile file = EnteFile();
file.lAsset = asset;
file.deviceFolder = asset.relativePath;
file.location =
Location(latitude: asset.latitude, longitude: asset.longitude);
file.fileType = enteTypeFromAsset(asset);
file.creationTime = asset.createDateTime.microsecondsSinceEpoch;
file.modificationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
return file;
}
static EnteFile fromRemoteAsset(
RemoteAsset rAsset,
CollectionFile collection, {
AssetEntity? lAsset,
}) {
final EnteFile file = EnteFile();
file.rAsset = rAsset;
file.cf = collection;
file.lAsset = lAsset;
file.ownerID = rAsset.ownerID;
// file.deviceFolder = rAsset.deviceFolder;
file.location = rAsset.location;
file.fileType = rAsset.fileType;
file.creationTime = rAsset.creationTime;
file.modificationTime = rAsset.modificationTime;
return file;
}
String? get localID => lAsset?.id ?? sharedAsset?.id;
int get remoteID {
if (rAsset != null) {
return rAsset!.id;
} else {
throw Exception("Remote ID is not set for the file");
}
}
String? get hash => rAsset?.hash;
int? get fileSubType => rAsset?.subType ?? lAsset?.subtype;
int? get uploadedFileID => rAsset?.id;
int? get durationInSec => rAsset?.durationInSec ?? lAsset?.duration;
String? get title => rAsset?.title ?? lAsset?.title;
int? get collectionID => cf?.collectionID;
Future<AssetEntity?> get getAsset {
if (localID == null) {
return Future.value(null);
@@ -127,132 +104,23 @@ class EnteFile {
return AssetEntity.fromId(localID!);
}
void applyMetadata(Map<String, dynamic> metadata) {
localID = metadata["localID"];
title = metadata["title"];
deviceFolder = metadata["deviceFolder"];
creationTime = metadata["creationTime"] ?? 0;
modificationTime = metadata["modificationTime"] ?? creationTime;
final latitude = double.tryParse(metadata["latitude"].toString());
final longitude = double.tryParse(metadata["longitude"].toString());
if (latitude == null || longitude == null) {
location = null;
} else {
location = Location(latitude: latitude, longitude: longitude);
}
fileType = getFileType(metadata["fileType"] ?? -1);
fileSubType = metadata["subType"] ?? -1;
duration = metadata["duration"] ?? 0;
exif = metadata["exif"];
hash = metadata["hash"];
// handle past live photos upload from web client
if (hash == null &&
fileType == FileType.livePhoto &&
metadata.containsKey('imageHash') &&
metadata.containsKey('videoHash')) {
// convert to imgHash:vidHash
hash =
'${metadata['imageHash']}$kLivePhotoHashSeparator${metadata['videoHash']}';
}
metadataVersion = metadata["version"] ?? 0;
}
Future<Map<String, dynamic>> getMetadataForUpload(
MediaUploadData mediaUploadData,
ParsedExifDateTime? exifTime,
) async {
final asset = await getAsset;
// asset can be null for files shared to app
if (asset != null) {
fileSubType = asset.subtype;
if (fileType == FileType.video) {
duration = asset.duration;
}
}
bool hasExifTime = false;
if (exifTime != null && exifTime.time != null) {
hasExifTime = true;
creationTime = exifTime.time!.microsecondsSinceEpoch;
}
if (mediaUploadData.exifData != null) {
mediaUploadData.isPanorama =
checkPanoramaFromEXIF(null, mediaUploadData.exifData);
}
if (mediaUploadData.isPanorama != true &&
fileType == FileType.image &&
mediaUploadData.sourceFile != null) {
try {
final xmpData = await getXmp(mediaUploadData.sourceFile!);
mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData);
} catch (_) {}
mediaUploadData.isPanorama ??= false;
}
// Try to get the timestamp from fileName. In case of iOS, file names are
// generic IMG_XXXX, so only parse it on Android devices
if (!hasExifTime && Platform.isAndroid && title != null) {
final timeFromFileName = parseDateTimeFromFileNameV2(title!);
if (timeFromFileName != null) {
// only use timeFromFileName if the existing creationTime and
// timeFromFilename belongs to different date.
// This is done because many times the fileTimeStamp will only give us
// the date, not time value but the photo_manager's creation time will
// contain the time.
final bool useFileTimeStamp = creationTime == null ||
!areFromSameDay(
creationTime!,
timeFromFileName.microsecondsSinceEpoch,
);
if (useFileTimeStamp) {
creationTime = timeFromFileName.microsecondsSinceEpoch;
}
}
}
hash = mediaUploadData.hashData?.fileHash;
return metadata;
}
Map<String, dynamic> get metadata {
final metadata = <String, dynamic>{};
metadata["localID"] = isSharedMediaToAppSandbox ? null : localID;
metadata["title"] = title;
metadata["deviceFolder"] = deviceFolder;
metadata["creationTime"] = creationTime;
metadata["modificationTime"] = modificationTime;
metadata["fileType"] = fileType.index;
if (location != null &&
location!.latitude != null &&
location!.longitude != null) {
metadata["latitude"] = location!.latitude;
metadata["longitude"] = location!.longitude;
}
if (fileSubType != null) {
metadata["subType"] = fileSubType;
}
if (duration != null) {
metadata["duration"] = duration;
}
if (hash != null) {
metadata["hash"] = hash;
}
if (metadataVersion != null) {
metadata["version"] = metadataVersion;
}
return metadata;
}
String get downloadUrl =>
FileUrl.getUrl(uploadedFileID!, FileUrlType.download);
String? get caption {
return pubMagicMetadata?.caption;
return rAsset?.caption;
}
String? debugCaption;
int? get fileSize {
if (rAsset != null) {
return rAsset!.fileSize;
}
return null;
}
String get displayName {
if (pubMagicMetadata != null && pubMagicMetadata!.editedName != null) {
return pubMagicMetadata!.editedName!;
if (rAsset != null) {
return rAsset!.title;
}
if (title == null && kDebugMode) _logger.severe('File title is null');
return title ?? '';
@@ -260,11 +128,17 @@ class EnteFile {
// return 0 if the height is not available
int get height {
return pubMagicMetadata?.h ?? 0;
if (rAsset != null) {
return rAsset!.height ?? 0;
}
return lAsset?.height ?? 0;
}
int get width {
return pubMagicMetadata?.w ?? 0;
if (rAsset != null) {
return rAsset!.width ?? 0;
}
return lAsset?.width ?? 0;
}
bool get hasDimensions {
@@ -273,15 +147,16 @@ class EnteFile {
// returns true if the file isn't available in the user's gallery
bool get isRemoteFile {
return localID == null && uploadedFileID != null;
return localID == null && isUploaded;
}
bool get isUploaded {
return uploadedFileID != null;
return rAsset != null;
}
bool get isSharedMediaToAppSandbox {
return localID != null && localID!.startsWith(sharedMediaIdentifier);
// returns true if the file is only available in the app's sandbox
bool get isInAppMedia {
return sharedAsset != null;
}
bool get hasLocation {
@@ -293,7 +168,7 @@ class EnteFile {
String toString() {
return '''File(generatedID: $generatedID, localID: $localID, title: $title,
type: $fileType, uploadedFileId: $uploadedFileID, modificationTime: $modificationTime,
ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)''';
ownerID: $ownerID, collectionID: $collectionID, updationTime: ${cf?.updatedAt})''';
}
@override
@@ -312,12 +187,7 @@ class EnteFile {
}
String get tag {
return "local_" +
localID.toString() +
":remote_" +
uploadedFileID.toString() +
":generated_" +
generatedID.toString();
return "local_$localID:remote_$uploadedFileID:generated_$generatedID";
}
String cacheKey() {
@@ -327,68 +197,27 @@ class EnteFile {
EnteFile copyWith({
int? generatedID,
int? uploadedFileID,
int? ownerID,
int? collectionID,
String? localID,
String? title,
String? deviceFolder,
int? creationTime,
int? modificationTime,
int? updationTime,
int? addedTime,
Location? location,
FileType? fileType,
int? fileSubType,
int? duration,
String? exif,
String? hash,
int? metadataVersion,
String? encryptedKey,
String? keyDecryptionNonce,
String? fileDecryptionHeader,
String? thumbnailDecryptionHeader,
String? metadataDecryptionHeader,
int? fileSize,
String? mMdEncodedJson,
int? mMdVersion,
MagicMetadata? magicMetadata,
String? pubMmdEncodedJson,
int? pubMmdVersion,
PubMagicMetadata? pubMagicMetadata,
}) {
return EnteFile()
..lAsset = lAsset
..rAsset = rAsset
..cf = cf
..generatedID = generatedID ?? this.generatedID
..uploadedFileID = uploadedFileID ?? this.uploadedFileID
..ownerID = ownerID ?? this.ownerID
..collectionID = collectionID ?? this.collectionID
..localID = localID ?? this.localID
..title = title ?? this.title
..deviceFolder = deviceFolder ?? this.deviceFolder
..creationTime = creationTime ?? this.creationTime
..modificationTime = modificationTime ?? this.modificationTime
..updationTime = updationTime ?? this.updationTime
..addedTime = addedTime ?? this.addedTime
..location = location ?? this.location
..fileType = fileType ?? this.fileType
..fileSubType = fileSubType ?? this.fileSubType
..duration = duration ?? this.duration
..exif = exif ?? this.exif
..hash = hash ?? this.hash
..metadataVersion = metadataVersion ?? this.metadataVersion
..encryptedKey = encryptedKey ?? this.encryptedKey
..keyDecryptionNonce = keyDecryptionNonce ?? this.keyDecryptionNonce
..fileDecryptionHeader = fileDecryptionHeader ?? this.fileDecryptionHeader
..thumbnailDecryptionHeader =
thumbnailDecryptionHeader ?? this.thumbnailDecryptionHeader
..metadataDecryptionHeader =
metadataDecryptionHeader ?? this.metadataDecryptionHeader
..fileSize = fileSize ?? this.fileSize
..mMdEncodedJson = mMdEncodedJson ?? this.mMdEncodedJson
..mMdVersion = mMdVersion ?? this.mMdVersion
..magicMetadata = magicMetadata ?? this.magicMetadata
..pubMmdEncodedJson = pubMmdEncodedJson ?? this.pubMmdEncodedJson
..pubMmdVersion = pubMmdVersion ?? this.pubMmdVersion
..pubMagicMetadata = pubMagicMetadata ?? this.pubMagicMetadata;
..fileType = fileType ?? this.fileType;
}
}

View File

@@ -6,7 +6,9 @@ enum FileType {
image,
video,
livePhoto,
other,
other;
bool get isVideo => this == FileType.video;
}
int getInt(FileType fileType) {
@@ -35,7 +37,7 @@ FileType getFileType(int fileType) {
}
}
FileType fileTypeFromAsset(AssetEntity asset) {
FileType enteTypeFromAsset(AssetEntity asset) {
FileType type = FileType.image;
switch (asset.type) {
case AssetType.image:

View File

@@ -0,0 +1,25 @@
class LocalAssetInfo {
final String id;
final String? hash;
final String? title;
final String? relativePath;
final int state;
LocalAssetInfo({
required this.id,
this.hash,
this.title,
this.relativePath,
required this.state,
});
factory LocalAssetInfo.fromRow(Map<String, Object?> row) {
return LocalAssetInfo(
id: row['id'] as String,
hash: row['hash'] as String?,
title: row['title'] as String?,
relativePath: row['relative_path'] as String?,
state: row['scan_state'] as int,
);
}
}

View File

@@ -0,0 +1,190 @@
import "dart:typed_data";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/file/file_type.dart";
import "package:photos/models/location/location.dart";
import "package:photos/models/metadata/common_keys.dart";
import "package:photos/models/metadata/file_magic.dart";
// Represents the remote asset stored in the database
// Note: Ensure that the fields in this class matches the database schema
// (remote_db -> files table). Keep the field order consistent with the schema.
class RemoteAsset {
final int id;
final int ownerID;
final Uint8List fileHeader;
final Uint8List thumbHeader;
final int creationTime;
final int modificationTime;
final int type;
final int subType;
final String title;
final int? fileSize;
final String? hash;
final int? visibility;
final int? durationInSec;
final Location? location;
final int? height;
final int? width;
final int? noThumb;
final int? sv;
final int? mediaType;
final int? motionVideoIndex;
String? caption;
final String? uploaderName;
RemoteAsset({
required this.id,
required this.ownerID,
required this.thumbHeader,
required this.fileHeader,
required this.subType,
required this.type,
required this.creationTime,
required this.modificationTime,
required this.title,
this.hash,
this.visibility,
this.durationInSec,
this.location,
this.height,
this.width,
this.sv,
this.motionVideoIndex,
this.noThumb,
this.mediaType,
this.uploaderName,
this.fileSize,
this.caption,
});
// Factory constructor for creating from metadata (if needed for migration)
factory RemoteAsset.fromMetadata({
required int id,
required int ownerID,
required Uint8List thumbHeader,
required Uint8List fileHeader,
required Metadata metadata,
Metadata? privateMetadata,
Metadata? publicMetadata,
Info? info,
}) {
return RemoteAsset(
id: id,
ownerID: ownerID,
thumbHeader: thumbHeader,
fileHeader: fileHeader,
creationTime: publicMetadata?.data[editTimeKey] ??
metadata.data['creationTime'] ??
0,
title: publicMetadata?.data[editNameKey] ?? metadata.data['title'] ?? "",
modificationTime: metadata.data["modificationTime"] ??
publicMetadata?.data[editTimeKey] ??
metadata.data['creationTime'] ??
0,
hash: metadata.data['hash'],
location: RemoteAsset.parseLocation(publicMetadata, metadata),
durationInSec: metadata.data['duration'] ?? 0,
fileSize: info?.fileSize,
subType: metadata.data['subType'] ?? -1,
type: metadata.data['fileType'] ?? -1,
height: safeParseInt(publicMetadata?.data[heightKey], heightKey),
width: safeParseInt(publicMetadata?.data[widthKey], widthKey),
sv: publicMetadata?.data[streamVersionKey],
motionVideoIndex: publicMetadata?.data[motionVideoIndexKey],
noThumb: (publicMetadata?.data[noThumbKey] ??
metadata.data["hasStaticThumbnail"] ??
false)
? 1
: 0,
caption: publicMetadata?.data[captionKey],
mediaType: publicMetadata?.data[mediaTypeKey],
uploaderName: publicMetadata?.data[uploaderNameKey],
visibility: privateMetadata?.data[magicKeyVisibility],
);
}
RemoteAsset copyWith({
int? id,
int? ownerID,
Uint8List? thumbHeader,
Uint8List? fileHeader,
int? subType,
int? type,
int? creationTime,
int? modificationTime,
String? title,
String? hash,
int? visibility,
int? durationInSec,
Location? location,
int? height,
int? width,
int? sv,
int? motionVideoIndex,
int? noThumb,
int? mediaType,
String? deviceFolder,
String? uploaderName,
int? fileSize,
String? caption,
}) {
return RemoteAsset(
id: id ?? this.id,
ownerID: ownerID ?? this.ownerID,
thumbHeader: thumbHeader ?? this.thumbHeader,
fileHeader: fileHeader ?? this.fileHeader,
subType: subType ?? this.subType,
type: type ?? this.type,
creationTime: creationTime ?? this.creationTime,
modificationTime: modificationTime ?? this.modificationTime,
title: title ?? this.title,
hash: hash ?? this.hash,
visibility: visibility ?? this.visibility,
durationInSec: durationInSec ?? this.durationInSec,
location: location ?? this.location,
height: height ?? this.height,
width: width ?? this.width,
sv: sv ?? this.sv,
motionVideoIndex: motionVideoIndex ?? this.motionVideoIndex,
noThumb: noThumb ?? this.noThumb,
mediaType: mediaType ?? this.mediaType,
uploaderName: uploaderName ?? this.uploaderName,
fileSize: fileSize ?? this.fileSize,
caption: caption ?? this.caption,
);
}
bool get isArchived {
return visibility == archiveVisibility;
}
FileType get fileType {
return getFileType(type);
}
static Location? parseLocation(Metadata? publicMetadata, Metadata metadata) {
if (publicMetadata?.data[latKey] != null) {
return Location(
latitude: publicMetadata!.data[latKey],
longitude: publicMetadata!.data[longKey],
);
}
if (metadata.data['latitude'] == null ||
metadata.data['longitude'] == null) {
return null;
}
final latitude = double.tryParse(metadata.data["latitude"].toString());
final longitude = double.tryParse(metadata.data["longitude"].toString());
if (latitude == null ||
longitude == null ||
(latitude == 0.0 && longitude == 0.0)) {
return null;
} else {
return Location(latitude: latitude, longitude: longitude);
}
}
}

View File

@@ -0,0 +1,27 @@
import "dart:typed_data";
class CollectionFile {
final int collectionID;
final int fileID;
final Uint8List encFileKey;
final Uint8List encFileKeyNonce;
final int updatedAt;
final int createdAt;
CollectionFile({
required this.collectionID,
required this.fileID,
required this.encFileKey,
required this.encFileKeyNonce,
required this.updatedAt,
required this.createdAt,
});
CollectionFile.fromMap(Map<String, dynamic> map)
: collectionID = map["collection_id"] as int,
fileID = map["file_id"] as int,
encFileKey = map["enc_key"] as Uint8List,
encFileKeyNonce = map["enc_key_nonce"] as Uint8List,
updatedAt = map["updated_at"] as int,
createdAt = map["created_at"] as int;
}

View File

@@ -0,0 +1,57 @@
class RLMapping {
final int remoteUploadID;
final String localID;
final String? localCloudID;
final MatchType mappingType;
RLMapping({
required this.remoteUploadID,
required this.localID,
required this.localCloudID,
required this.mappingType,
});
List<Object?> get rowValues => [
remoteUploadID,
localID,
localCloudID,
mappingType.name,
];
}
enum MatchType {
localID,
cloudID,
deviceUpload,
deviceHashMatched,
}
extension MappingTypeExtension on MatchType {
String get name {
switch (this) {
case MatchType.localID:
return "localID";
case MatchType.cloudID:
return "cloudID";
case MatchType.deviceUpload:
return "deviceUpload";
case MatchType.deviceHashMatched:
return "deviceHashMatched";
}
}
static MatchType fromName(String name) {
switch (name) {
case "localID":
return MatchType.localID;
case "cloudID":
return MatchType.cloudID;
case "deviceUpload":
return MatchType.deviceUpload;
case "deviceHashMatched":
return MatchType.deviceHashMatched;
default:
throw Exception("Unknown mapping type: $name");
}
}
}

View File

@@ -1,14 +0,0 @@
import 'package:photos/models/file/file.dart';
class TrashFile extends EnteFile {
// time when file was put in the trash for first time
late int createdAt;
// for non-deleted trash items, updateAt is usually equal to the latest time
// when the file was moved to trash
late int updateAt;
// time after which will will be deleted from trash & user's storage usage
// will go down
late int deleteBy;
}

View File

@@ -21,7 +21,7 @@ class FilesSplit {
ownedByOtherUsers = [],
pendingUploads = [];
for (var f in files) {
if (f.ownerID == null || f.uploadedFileID == null) {
if (f.ownerID == null || !f.isUploaded) {
pendingUploads.add(f);
} else if (f.ownerID == currentUserID) {
ownedByCurrentUser.add(f);

View File

@@ -1,4 +1,4 @@
import 'package:photos/models/file/trash_file.dart';
import "package:photos/models/api/diff/diff.dart";
const kIgnoreReasonTrash = "trash";
@@ -10,21 +10,21 @@ class IgnoredFile {
IgnoredFile(this.localID, this.title, this.deviceFolder, this.reason);
static fromTrashItem(TrashFile? trashFile) {
if (trashFile == null) return null;
if (trashFile.localID == null ||
trashFile.localID!.isEmpty ||
trashFile.title == null ||
trashFile.title!.isEmpty ||
trashFile.deviceFolder == null ||
trashFile.deviceFolder!.isEmpty) {
static fromTrashItem(DiffItem? item) {
if (item == null) return null;
final fileItem = item.fileItem;
if (fileItem.localID == null ||
fileItem.localID!.isEmpty ||
fileItem.nonEditedTitle.isEmpty ||
fileItem.deviceFolder == null ||
fileItem.deviceFolder!.isEmpty) {
return null;
}
return IgnoredFile(
trashFile.localID,
trashFile.title,
trashFile.deviceFolder,
fileItem.localID,
fileItem.nonEditedTitle,
fileItem.deviceFolder,
kIgnoreReasonTrash,
);
}

View File

@@ -0,0 +1,57 @@
import "package:photos/models/file/file_type.dart";
import "package:photos/models/location/location.dart";
class LocalAsset {
/// The ID of the asset.
/// AssetEntity.id
final String id;
final FileType type;
final int subType;
final int width;
final int height;
final int durationInSec;
final int orientation;
/// Whether the asset is favorite on the device.
/// See also:
/// * [AssetEntity.isFavorite]
final bool isFavorite;
final String title;
/// See [AssetEntity.relativePath]
final String? relativePath;
final int createdAt;
final int modifiedAt;
// /// See [AssetEntity.relativePath]
final String? mimeType;
final Location? location;
final int scanState;
final String? hash;
final int? size;
LocalAsset({
required this.id,
required this.type,
required this.subType,
required this.width,
required this.height,
required this.durationInSec,
required this.orientation,
required this.isFavorite,
required this.title,
this.relativePath,
required this.createdAt,
required this.modifiedAt,
this.mimeType,
this.location,
required this.scanState,
this.hash,
this.size,
});
}

View File

@@ -0,0 +1,21 @@
import "package:photos/models/file/file_type.dart";
class AssetUploadQueue {
final String id;
final int destCollectionId;
final String? pathId;
final int ownerId;
final bool manual;
late FileType? fileType;
late int? createdAt;
AssetUploadQueue({
required this.id,
required this.destCollectionId,
required this.pathId,
required this.ownerId,
this.manual = false,
this.fileType,
this.createdAt,
});
}

View File

@@ -0,0 +1,45 @@
import "package:photos/models/location/location.dart";
class MetadataResult {
final DroidMetadata? droid;
final IOSMetadata? iOSMetadata;
final int processedState;
MetadataResult({
this.droid,
this.iOSMetadata,
required this.processedState,
});
}
class DroidMetadata {
int creationTime;
int modificationTime;
String hash;
int size;
Location? location;
bool? isPanorama;
int? mviIndex;
DroidMetadata({
required this.hash,
required this.size,
required this.creationTime,
required this.modificationTime,
this.mviIndex,
this.location,
this.isPanorama,
});
}
class IOSMetadata {
// https://developer.apple.com/documentation/photos/phcloudidentifier
// Bulk mapping from local to cloud identifiers & vice versa
String? cloudIdentifier;
// https://developer.apple.com/documentation/photos/phassetsourcetype
int? sourceType;
bool? hasAdjustments;
String? adjustmentFormatIdentifier;
bool? representsBurst;
String? burstIdentifier;
int? burstSelectionTypes;
}

View File

@@ -0,0 +1,20 @@
import "package:photos/models/upload_strategy.dart";
class PathConfig {
final String pathID;
final int ownerId;
// the target collection ID where the assets in this path will be uploaded
// if null, the client will try to map to existing collection based on the path
// or create a new collection if no mapping exists
final int? destCollectionID;
final bool shouldBackup;
final UploadStrategy uploadStrategy;
PathConfig(
this.pathID,
this.ownerId,
this.destCollectionID,
this.shouldBackup,
this.uploadStrategy,
);
}

View File

@@ -0,0 +1,51 @@
import "package:photos/models/file/file_type.dart";
class SharedAsset {
final String id;
final String name;
final FileType type;
final int creationTime;
final int durationInSeconds;
final int destCollectionID;
final int ownerID;
final double? latitude;
final double? longitude;
SharedAsset({
required this.id,
required this.name,
required this.type,
required this.creationTime,
required this.durationInSeconds,
required this.destCollectionID,
required this.ownerID,
this.latitude,
this.longitude,
});
List<Object?> get rowProps => [
id,
name,
getInt(type),
creationTime,
durationInSeconds,
destCollectionID,
ownerID,
latitude,
longitude,
];
factory SharedAsset.fromRow(Map<String, dynamic> map) {
return SharedAsset(
id: map['id'] as String,
name: map['name'] as String,
type: getFileType(['type'] as int),
creationTime: map['creation_time'] as int,
durationInSeconds: map['duration_in_seconds'] as int,
destCollectionID: map['dest_collection_id'] as int,
ownerID: map['owner_id'] as int,
latitude: map['latitude'] as double?,
longitude: map['longitude'] as double?,
);
}
}

View File

@@ -144,8 +144,8 @@ class ToShowMemory {
return ToShowMemory(
memory.title,
memory.memories
.where((m) => m.file.uploadedFileID != null)
.map((m) => m.file.uploadedFileID!)
.where((m) => m.file.isUploaded)
.map((m) => m.file.remoteID)
.toList(),
memory.type,
memory.firstDateToShow,

View File

@@ -1,44 +1,22 @@
import "dart:convert";
import "package:flutter/cupertino.dart";
import 'package:photos/models/metadata/common_keys.dart';
const editTimeKey = 'editedTime';
const editNameKey = 'editedName';
const latKey = "lat";
const longKey = "long";
const noThumbKey = "noThumb";
const captionKey = "caption";
const uploaderNameKey = "uploaderName";
const widthKey = 'w';
const heightKey = 'h';
const streamVersionKey = 'sv';
const mediaTypeKey = 'mediaType';
const latKey = "lat";
const longKey = "long";
const motionVideoIndexKey = "mvi";
const noThumbKey = "noThumb";
const dateTimeKey = 'dateTime';
const offsetTimeKey = 'offsetTime';
class MagicMetadata {
// 0 -> visible
// 1 -> archived
// 2 -> hidden etc?
int visibility;
MagicMetadata({required this.visibility});
factory MagicMetadata.fromEncodedJson(String encodedJson) =>
MagicMetadata.fromJson(jsonDecode(encodedJson));
factory MagicMetadata.fromJson(dynamic json) => MagicMetadata.fromMap(json);
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
return MagicMetadata(
visibility: map[magicKeyVisibility] ?? visibleVisibility,
);
}
}
class PubMagicMetadata {
int? editedTime;
String? editedName;
@@ -116,12 +94,12 @@ class PubMagicMetadata {
sv: safeParseInt(map[streamVersionKey], streamVersionKey),
);
}
static int? safeParseInt(dynamic value, String key) {
if (value == null) return null;
if (value is int) return value;
debugPrint("PubMagicMetadata key: $key Unexpected value: $value");
if (value is String) return int.tryParse(value);
return null;
}
}
int? safeParseInt(dynamic value, String key) {
if (value == null) return null;
if (value is int) return value;
debugPrint("PubMagicMetadata key: $key Unexpected value: $value");
if (value is String) return int.tryParse(value);
return null;
}

View File

@@ -2,8 +2,8 @@ import "dart:convert";
import "package:photos/models/ml/ml_versions.dart";
class ClipEmbedding {
final int fileID;
class ClipEmbedding<T> {
final T fileID;
final List<double> embedding;
int version;
@@ -15,7 +15,7 @@ class ClipEmbedding {
required this.version,
});
factory ClipEmbedding.empty(int fileID) {
factory ClipEmbedding.empty(T fileID) {
return ClipEmbedding(
fileID: fileID,
embedding: <double>[],

View File

@@ -16,7 +16,7 @@ class FileInfo {
});
}
class Face {
class Face<T> {
final String faceID;
final List<double> embedding;
Detection detection;
@@ -26,7 +26,7 @@ class Face {
///#region Local DB fields
// This is not stored on the server, using it for local DB row
FileInfo? fileInfo;
final int fileID;
final T fileID;
///#endregion
@@ -48,7 +48,7 @@ class Face {
factory Face.fromFaceResult(
FaceResult faceResult,
int fileID,
T fileID,
Dimensions decodedDimensions,
) {
final detection = Detection(
@@ -81,7 +81,7 @@ class Face {
);
}
factory Face.empty(int fileID, {bool error = false}) {
static Face<T> empty<T>(T fileID, {bool error = false}) {
return Face(
"${fileID}_0_0_0_0",
fileID,
@@ -92,9 +92,9 @@ class Face {
);
}
factory Face.fromJson(Map<String, dynamic> json) {
static Face<T> fromJson<T>(Map<String, dynamic> json) {
final String faceID = json['faceID'] as String;
final int fileID = getFileIdFromFaceId<int>(faceID);
final T fileID = getFileIdFromFaceId<T>(faceID);
return Face(
faceID,
fileID,

View File

@@ -3,9 +3,9 @@ import "package:photos/services/machine_learning/face_ml/face_filtering/face_fil
import "package:photos/services/machine_learning/ml_result.dart";
import "package:photos/utils/standalone/parse.dart";
class FaceWithoutEmbedding {
class FaceWithoutEmbedding<T> {
final String faceID;
final int fileID;
final T fileID;
Detection detection;
final double score;
final double blur;
@@ -24,10 +24,10 @@ class FaceWithoutEmbedding {
this.blur,
);
factory FaceWithoutEmbedding.fromJson(Map<String, dynamic> json) {
static FaceWithoutEmbedding fromJson<T>(Map<String, dynamic> json) {
final String faceID = json['faceID'] as String;
final int fileID = getFileIdFromFaceId<int>(faceID);
return FaceWithoutEmbedding(
final T fileID = getFileIdFromFaceId<T>(faceID);
return FaceWithoutEmbedding<T>(
faceID,
fileID,
parseIntOrDoubleAsDouble(json['score'])!,

View File

@@ -223,6 +223,7 @@ extension SectionTypeExtensions on SectionType {
return SearchService.instance.getAllFace(limit);
case SectionType.magic:
return SearchService.instance.getMagicSectionResults(context!);
case SectionType.location:
return SearchService.instance.getAllLocationTags(limit);

View File

@@ -69,12 +69,11 @@ class SelectedFiles extends ChangeNotifier {
}
bool _isMatch(EnteFile first, EnteFile second) {
if (first.generatedID != null && second.generatedID != null) {
if (first.generatedID == second.generatedID) {
return true;
}
} else if (first.uploadedFileID != null && second.uploadedFileID != null) {
return first.uploadedFileID == second.uploadedFileID;
if (first.isUploaded && second.isUploaded) {
return first.remoteID == second.remoteID;
} else if (first.lAsset != null && second.lAsset != null) {
return first.lAsset!.id == second.lAsset!.id;
}
return false;
}

View File

@@ -0,0 +1,54 @@
import "package:photos/models/file/file.dart";
import "package:photos/models/local/asset_upload_queue.dart";
class AssetUploadCandidates {
// own presents files that needs to be uploaded to the user's own collection
// shared presents files that needs to be uploaded to the user's shared collection
// unknwon presents files that needs to be uploaded to a collection that is not found
List<(AssetUploadQueue, EnteFile)> own = [];
List<(AssetUploadQueue, EnteFile)> shared = [];
List<(AssetUploadQueue, EnteFile)> unknwon = [];
int ignored = 0;
int skippedVideos = 0;
int forcedVideos = 0;
bool includeVideos = false;
AssetUploadCandidates({
this.own = const [],
this.shared = const [],
this.unknwon = const [],
this.ignored = 0,
this.skippedVideos = 0,
this.forcedVideos = 0,
this.includeVideos = false,
});
/// debugPrint entries in a readable format, only include counts for list, and non-default values
/// for other fields
/// This is used for debugging purposes.
/// @return a string representation of the AssetUploadCandidates
@override
String toString() {
final StringBuffer sb = StringBuffer();
sb.writeln("AssetUploadCandidates:");
if (own.isNotEmpty) {
sb.writeln(" ownedCollection: ${own.length}");
}
if (shared.isNotEmpty) {
sb.writeln(" sharedCollection: ${shared.length}");
}
if (unknwon.isNotEmpty) {
sb.writeln(" missingCollection: ${unknwon.length}");
}
if (ignored > 0) {
sb.writeln(" ignored: $ignored");
}
if (skippedVideos > 0) {
sb.writeln(" skippedVideos: $skippedVideos");
}
if (forcedVideos > 0) {
sb.writeln(" forcedVideos: $forcedVideos");
}
sb.writeln(" includeVideos: $includeVideos");
return sb.toString();
}
}

View File

@@ -0,0 +1,29 @@
import "dart:async";
import "package:photos/models/file/file.dart";
import "package:photos/models/local/asset_upload_queue.dart";
class FileUploadItem {
final EnteFile file;
final int collectionID;
final Completer<EnteFile> completer;
final AssetUploadQueue? assetQueue;
UploadStatus status;
FileUploadItem(
this.file,
this.collectionID,
this.completer, {
this.assetQueue,
this.status = UploadStatus.notStarted,
});
String get lockKey => assetQueue?.id ?? file.localID!;
}
enum UploadStatus { notStarted, inProgress, inBackground, completed }
enum ProcessType {
background,
foreground,
}

View File

@@ -0,0 +1,61 @@
import "dart:io";
import "dart:typed_data";
import "package:photo_manager/photo_manager.dart";
import "package:photos/models/file/file_type.dart";
import "package:photos/models/local/shared_asset.dart";
// UploadMedia holds information about the actual media that's being uploaded.
// Apart from hash, this doesn't contain any metadata that will be reported to the server
// as part of the upload request. The metadata is handled separately in the UploadData class.
class UploadMedia {
final File uploadFile;
final FileType fileType;
final Uint8List? thumbnail;
final bool isDeleted;
final String hash;
final String? livePhotoImage;
final String? livePhotoVideo;
final AssetEntity? localAsset;
final SharedAsset? sharedAsset;
UploadMedia(
this.uploadFile,
this.thumbnail,
this.isDeleted,
this.fileType,
this.hash, {
this.livePhotoVideo,
this.livePhotoImage,
this.localAsset,
this.sharedAsset,
}) : assert(
(localAsset != null && sharedAsset == null) ||
(localAsset == null && sharedAsset != null),
'Either localAsset or sharedAsset must be present, but not both',
),
assert(
fileType == FileType.livePhoto
? (livePhotoImage != null && livePhotoVideo != null)
: (livePhotoImage == null && livePhotoVideo == null),
'For live photos, both livePhotoImage and livePhotoVideo must be present. For other file types, both must be null',
);
// delete the original file that's fetched from the device. Also, clean up
// the shared asset if the file is already uploaded.
Future<void> delete() async {
if (uploadFile.existsSync() && (Platform.isIOS || sharedAsset != null)) {
await uploadFile.delete();
}
if (livePhotoImage != null && livePhotoVideo != null) {
final livePhotoImageFile = File(livePhotoImage!);
final livePhotoVideoFile = File(livePhotoVideo!);
if (livePhotoImageFile.existsSync()) {
await livePhotoImageFile.delete();
}
if (livePhotoVideoFile.existsSync()) {
await livePhotoVideoFile.delete();
}
}
}
}

View File

@@ -0,0 +1,9 @@
class FileUpdateResponse {
final int id;
final int updationTime;
FileUpdateResponse({
required this.id,
required this.updationTime,
});
}

View File

@@ -0,0 +1,11 @@
class UploadMetadaData {
final Map<String, dynamic> defaultMetadata;
final Map<String, dynamic>? publicMetadata;
final int? currentPublicMetadataVersion;
UploadMetadaData({
required this.defaultMetadata,
required this.publicMetadata,
required this.currentPublicMetadataVersion,
});
}

View File

@@ -0,0 +1,217 @@
import "dart:convert";
import "dart:core";
import "dart:io";
import 'dart:typed_data';
import "package:computer/computer.dart";
import 'package:ente_crypto/ente_crypto.dart';
import "package:exif_reader/exif_reader.dart";
import 'package:logging/logging.dart';
import "package:motion_photos/motion_photos.dart";
import 'package:photo_manager/photo_manager.dart';
import "package:photos/db/remote/table/files_table.dart";
import "package:photos/models/api/diff/diff.dart";
import "package:photos/models/api/metadata.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/models/location/location.dart";
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/module/upload/model/media.dart";
import "package:photos/module/upload/model/upload_data.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/local/metadata/metadata.service.dart";
import "package:photos/utils/exif_util.dart";
import "package:photos/utils/panorama_util.dart";
final _logger = Logger("FileUtil");
// in Version 1, live photo hash is stored as zip's hash.
// in V2: LivePhoto hash is stored as imgHash:vidHash
const kCurrentMetadataVersion = 2;
Future<int?> motionVideoIndex(Map<String, dynamic> args) async {
final String path = args['path'];
return (await MotionPhotos(path).getMotionVideoIndex())?.start;
}
Future<UploadMetadaData> getUploadMetadata(
UploadMedia uploadMedia,
EnteFile file,
) async {
final FileType fileType = file.fileType;
Map<String, IfdTag>? exifData;
if (fileType == FileType.image) {
exifData = await readExifAsync(uploadMedia.uploadFile);
} else if (fileType == FileType.livePhoto) {
final imageFile = File(uploadMedia.livePhotoImage!);
exifData = await readExifAsync(imageFile);
}
final ParsedExifDateTime? exifTime =
exifData != null ? parseExifTime(exifData) : null;
bool? isPanorama;
int? mviIndex;
if (fileType == FileType.image) {
isPanorama = isPanoFromExif(exifData);
if (isPanorama != true) {
try {
final xmpData = await getXmp(uploadMedia.uploadFile);
isPanorama = isPanoFromXmp(xmpData);
} catch (_) {}
isPanorama ??= false;
}
if (Platform.isAndroid) {
try {
mviIndex = await Computer.shared().compute(
motionVideoIndex,
param: {'path': uploadMedia.uploadFile.path},
taskName: 'motionPhotoIndex',
);
} catch (e) {
_logger.severe('error while detecthing motion photo start index', e);
}
}
}
final Map<String, dynamic> defaultMetadata = await getMetadata(
uploadMedia,
exifTime,
exifData,
file,
);
Metadata? existingPublicMetadata;
if (file.rAsset != null) {
final result =
await remoteDB.getIDToMetadata({file.rAsset!.id}, public: true);
existingPublicMetadata = result[file.rAsset!.id];
}
final Map<String, dynamic> publicMetadata = _buildPublicMagicData(
exifTime,
existingPublicMetadata,
width: uploadMedia.localAsset?.width,
height: uploadMedia.localAsset?.height,
isPanorama: isPanorama,
motionPhotoStartIndex: mviIndex,
noThumbnail: uploadMedia.thumbnail == null,
);
return UploadMetadaData(
defaultMetadata: defaultMetadata,
publicMetadata: publicMetadata.isEmpty ? null : publicMetadata,
currentPublicMetadataVersion: existingPublicMetadata?.version,
);
}
Future<Map<String, dynamic>> getMetadata(
UploadMedia uploadMedia,
ParsedExifDateTime? exifTime,
Map<String, IfdTag>? exifData,
EnteFile file,
) async {
final AssetEntity? asset = uploadMedia.localAsset;
final FileType fileType = file.fileType;
final String? deviceFolder = file.deviceFolder;
int? duration;
final (int creationTime, int modificationTime) =
LocalMetadataService.computeCreationAndModification(
asset,
exifData,
);
String? title = file.title;
final Location? location = await LocalMetadataService.detectLocation(
fileType.isVideo,
asset,
uploadMedia.uploadFile,
exifData,
);
// asset can be null for files shared to app
if (asset != null) {
if (asset.type == AssetType.video) {
duration = asset.duration;
}
if (title == null || title.isEmpty) {
_logger.warning("Title was missing ${file.tag}");
title = await asset.titleAsync;
}
}
final metadata = <String, dynamic>{
"localID": asset?.id,
"hash": uploadMedia.hash,
"version": kCurrentMetadataVersion,
"title": title,
"deviceFolder": deviceFolder,
"creationTime": creationTime,
"modificationTime": modificationTime,
"fileType": fileType.index,
};
if (asset != null) {
metadata["subType"] = asset.subtype;
}
if (Location.isValidLocation(location)) {
metadata["latitude"] = location!.latitude;
metadata["longitude"] = location.longitude;
}
if (duration != null) {
metadata["duration"] = duration;
}
return metadata;
}
Map<String, dynamic> _buildPublicMagicData(
ParsedExifDateTime? parsedExifTime,
Metadata? existingPublicMetadata, {
required int? width,
required int? height,
required bool? isPanorama,
required int? motionPhotoStartIndex,
required bool noThumbnail,
}) {
final Map<String, dynamic> pubMetadata = {};
if ((height ?? 0) != 0 && (width ?? 0) != 0) {
pubMetadata[heightKey] = height;
pubMetadata[widthKey] = width;
}
pubMetadata[mediaTypeKey] = isPanorama == true ? 1 : 0;
if (motionPhotoStartIndex != null) {
pubMetadata[motionVideoIndexKey] = motionPhotoStartIndex;
}
if (noThumbnail) {
pubMetadata[noThumbKey] = true;
}
if (parsedExifTime?.dateTime != null) {
pubMetadata[dateTimeKey] = parsedExifTime!.dateTime;
}
if (parsedExifTime?.offsetTime != null) {
pubMetadata[offsetTimeKey] = parsedExifTime!.offsetTime;
}
final Map<String, dynamic> jsonToUpdate =
existingPublicMetadata?.data ?? <String, dynamic>{};
pubMetadata.forEach((key, value) {
jsonToUpdate[key] = value;
});
return jsonToUpdate;
}
Future<MetadataRequest?> getPubMetadataRequest(
Map<String, dynamic> jsonToUpdate,
Uint8List fileKey,
int? publicMetadataVersion,
) async {
if (jsonToUpdate.isEmpty) {
return null;
}
final int currentVersion = (publicMetadataVersion ?? 0);
final encryptedMMd = await CryptoUtil.encryptChaCha(
utf8.encode(jsonEncode(jsonToUpdate)),
fileKey,
);
return MetadataRequest(
version: currentVersion == 0 ? 1 : currentVersion,
count: jsonToUpdate.length,
data: CryptoUtil.bin2base64(encryptedMMd.encryptedData!),
header: CryptoUtil.bin2base64(encryptedMMd.header!),
);
}

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