ok con video e foto in galleria aves
Some checks failed
Quality check / Flutter analysis (push) Has been cancelled
Quality check / CodeQL analysis (java-kotlin) (push) Has been cancelled

This commit is contained in:
FabioMich66 2026-03-17 12:19:38 +01:00
parent 4925c6e3eb
commit 084fa184da
101 changed files with 42959 additions and 5475 deletions

View file

@ -48,8 +48,13 @@ lib/widgets/viewer/visual/entry_page_view.dart
```
```
lib/widgets/viewer/visual/vector.dart viewer di altri formati immagine in aves
lib/widgets/viewer/video/video_view.dart viewer di video in avez
lib/widgets/viewer/visual/video/video_view.dart
```
per trovare un file
```
find lib -type f -name "vector.dart"
```
salvare il DB
```

2058
address.csv Normal file

File diff suppressed because it is too large Load diff

102
after.csv Normal file
View file

@ -0,0 +1,102 @@
id,remoteId,uri,provider,trashed,sourceMimeType,sizeBytes,remoteWidth,remoteHeight,remoteRotation,dateModifiedMillis
1,b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e,aves-remote://rid/b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e,json@patachina,0,image/jpeg,3758888,4032,3024,0,1503481740000
2,09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b,aves-remote://rid/09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b,json@patachina,0,image/jpeg,3984165,4032,3024,0,1503493700000
3,dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7,aves-remote://rid/dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7,json@patachina,0,image/jpeg,4338120,5326,3950,0,1503496121000
4,751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0,aves-remote://rid/751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0,json@patachina,0,image/jpeg,3159006,4032,3024,0,1503496785000
5,73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937,aves-remote://rid/73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937,json@patachina,0,image/jpeg,3338305,4032,3024,0,1503496787000
6,f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8,aves-remote://rid/f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8,json@patachina,0,image/jpeg,2815021,4032,3024,0,1503496997000
7,90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe,aves-remote://rid/90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe,json@patachina,0,image/jpeg,3207260,4032,3024,0,1503497854000
8,4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935,aves-remote://rid/4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935,json@patachina,0,image/jpeg,2674913,4032,3024,0,1503497873000
9,1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f,aves-remote://rid/1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f,json@patachina,0,image/jpeg,2788605,4032,3024,0,1503498970000
10,14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2,aves-remote://rid/14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2,json@patachina,0,image/jpeg,3447883,4032,3024,90,1503504834000
11,7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41,aves-remote://rid/7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41,json@patachina,0,image/jpeg,3513469,4032,3024,0,1503505103000
12,4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c,aves-remote://rid/4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c,json@patachina,0,image/jpeg,2353262,4032,3024,0,1503509034000
13,490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750,aves-remote://rid/490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750,json@patachina,0,image/jpeg,2912549,4032,3024,90,1503510106000
14,1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be,aves-remote://rid/1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be,json@patachina,0,image/jpeg,2351855,4032,3024,90,1503510129000
15,486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3,aves-remote://rid/486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3,json@patachina,0,image/jpeg,1869483,4032,3024,0,1503512829000
16,69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a,aves-remote://rid/69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a,json@patachina,0,image/jpeg,1800632,4032,3024,90,1503512842000
17,1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2,aves-remote://rid/1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2,json@patachina,0,image/jpeg,1643315,4032,3024,90,1503514391000
18,7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a,aves-remote://rid/7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a,json@patachina,0,image/jpeg,1450013,4032,3024,90,1503514403000
19,35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac,aves-remote://rid/35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac,json@patachina,0,image/jpeg,1606095,4032,3024,90,1503514433000
20,48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274,aves-remote://rid/48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274,json@patachina,0,image/jpeg,1839090,4032,3024,90,1503518935000
21,10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134,aves-remote://rid/10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134,json@patachina,0,image/jpeg,2013689,4032,3024,90,1503519976000
22,c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969,aves-remote://rid/c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969,json@patachina,0,image/jpeg,1557955,3724,2096,90,1773491614087
23,1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90,aves-remote://rid/1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90,json@patachina,0,image/jpeg,2667599,4032,3024,0,1503563815000
24,bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0,aves-remote://rid/bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0,json@patachina,0,image/jpeg,2698118,4032,3024,0,1503563819000
25,7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f,aves-remote://rid/7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f,json@patachina,0,image/jpeg,2656457,4032,3024,0,1503563838000
26,29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b,aves-remote://rid/29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b,json@patachina,0,image/jpeg,4654237,4032,3024,90,1503583782000
27,ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839,aves-remote://rid/ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839,json@patachina,0,image/jpeg,3760564,4032,3024,0,1503584553000
28,c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6,aves-remote://rid/c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6,json@patachina,0,image/jpeg,3627847,4032,3024,0,1503594928000
29,f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f,aves-remote://rid/f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f,json@patachina,0,image/jpeg,1900418,4032,3024,90,1503605922000
30,5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e,aves-remote://rid/5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e,json@patachina,0,image/jpeg,1944490,4032,3024,90,1503605929000
31,dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c,aves-remote://rid/dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c,json@patachina,0,image/jpeg,2020943,4032,3024,90,1503606518000
32,8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8,aves-remote://rid/8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8,json@patachina,0,image/jpeg,2632829,4032,3024,90,1503656054000
33,46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc,aves-remote://rid/46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc,json@patachina,0,image/jpeg,3300375,4032,3024,0,1503656159000
34,999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e,aves-remote://rid/999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e,json@patachina,0,image/jpeg,4461504,5532,3898,0,1503656554000
35,7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436,aves-remote://rid/7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436,json@patachina,0,image/jpeg,4964535,5232,3872,270,1503656644000
36,4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69,aves-remote://rid/4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69,json@patachina,0,image/jpeg,4837273,5300,3908,270,1503656673000
37,d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4,aves-remote://rid/d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4,json@patachina,0,image/jpeg,1697485,4032,3024,0,1503656711000
38,49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600,aves-remote://rid/49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600,json@patachina,0,image/jpeg,1680963,4032,3024,90,1503656725000
39,a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7,aves-remote://rid/a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7,json@patachina,0,image/jpeg,1502040,3024,4032,,1503656771000
40,fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6,aves-remote://rid/fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6,json@patachina,0,image/jpeg,1520788,4032,3024,90,1503656843000
41,c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93,aves-remote://rid/c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93,json@patachina,0,image/jpeg,4954269,8178,3754,0,1503657571000
42,7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d,aves-remote://rid/7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d,json@patachina,0,image/jpeg,2747267,4032,3024,90,1503666106000
43,23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f,aves-remote://rid/23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f,json@patachina,0,image/jpeg,2808395,4032,3024,0,1503666149000
44,0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843,aves-remote://rid/0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843,json@patachina,0,image/jpeg,3137643,4032,3024,0,1503666179000
45,64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f,aves-remote://rid/64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f,json@patachina,0,image/jpeg,2929880,4032,3024,90,1503667517000
46,8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39,aves-remote://rid/8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39,json@patachina,0,image/jpeg,2498451,4032,3024,90,1503667524000
47,ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8,aves-remote://rid/ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8,json@patachina,0,image/jpeg,3020402,4032,3024,90,1503669348000
48,a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a,aves-remote://rid/a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a,json@patachina,0,image/jpeg,2161605,4032,3024,90,1503671202000
49,7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a,aves-remote://rid/7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a,json@patachina,0,image/jpeg,2913137,4032,3024,90,1503686951000
50,9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3,aves-remote://rid/9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3,json@patachina,0,image/jpeg,3437757,4032,3024,90,1503686966000
51,df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a,aves-remote://rid/df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a,json@patachina,0,image/jpeg,1668522,3724,2096,90,1773491614875
52,f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6,aves-remote://rid/f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6,json@patachina,0,image/jpeg,1781263,3724,2096,90,1773491614881
53,68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56,aves-remote://rid/68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56,json@patachina,0,image/jpeg,973684,3763,1657,,1503695558000
54,6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4,aves-remote://rid/6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4,json@patachina,0,image/jpeg,1311890,3724,2096,0,1773491614903
55,dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66,aves-remote://rid/dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66,json@patachina,0,image/jpeg,4612510,7506,3870,0,1503734480000
56,4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf,aves-remote://rid/4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf,json@patachina,0,image/jpeg,4484747,5624,3958,0,1503734933000
57,4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc,aves-remote://rid/4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc,json@patachina,0,image/jpeg,2873334,8968,1560,,1503743055000
58,0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378,aves-remote://rid/0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378,json@patachina,0,image/jpeg,2168733,4032,3024,90,1503749114000
59,4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836,aves-remote://rid/4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836,json@patachina,0,image/jpeg,1891230,4032,3024,90,1503749116000
60,76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984,aves-remote://rid/76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984,json@patachina,0,image/jpeg,1766923,4032,3024,90,1503749303000
61,065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd,aves-remote://rid/065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd,json@patachina,0,image/jpeg,2935053,4032,3024,0,1503767620000
62,e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed,aves-remote://rid/e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed,json@patachina,0,image/jpeg,1852261,4032,3024,0,1503767749000
63,c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f,aves-remote://rid/c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f,json@patachina,0,image/jpeg,1936144,4032,3024,0,1503767754000
64,77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd,aves-remote://rid/77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd,json@patachina,0,image/jpeg,1843400,4032,3024,0,1503768183000
65,be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7,aves-remote://rid/be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7,json@patachina,0,image/jpeg,4221357,4658,3974,0,1503769227000
66,6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7,aves-remote://rid/6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7,json@patachina,0,image/jpeg,4179034,7305,2311,,1503769265000
67,ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db,aves-remote://rid/ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db,json@patachina,0,image/jpeg,1750381,1680,4030,0,1503769375000
68,3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf,aves-remote://rid/3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf,json@patachina,0,image/jpeg,2046548,4427,2389,,1503769385000
69,3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722,aves-remote://rid/3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722,json@patachina,0,image/jpeg,4328104,6438,3900,,1503769503000
70,50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027,aves-remote://rid/50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027,json@patachina,0,image/jpeg,703594,3946,2960,,1503779894000
71,df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e,aves-remote://rid/df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e,json@patachina,0,image/jpeg,468928,3745,1941,,1503779936000
72,2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070,aves-remote://rid/2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070,json@patachina,0,image/jpeg,2097953,4032,3024,90,1503822184000
73,bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba,aves-remote://rid/bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba,json@patachina,0,image/jpeg,2489649,4032,3024,0,1503823090000
74,a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9,aves-remote://rid/a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9,json@patachina,0,image/jpeg,3009764,4032,3024,0,1503830851000
75,915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f,aves-remote://rid/915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f,json@patachina,0,image/jpeg,2757600,4032,3024,0,1503830943000
76,0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82,aves-remote://rid/0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82,json@patachina,0,image/jpeg,3442902,4032,3024,0,1503830950000
77,a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9,aves-remote://rid/a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9,json@patachina,0,image/jpeg,3891743,4032,3024,90,1503833355000
78,308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df,aves-remote://rid/308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df,json@patachina,0,image/jpeg,1551495,3162,1743,,1503844208000
79,20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a,aves-remote://rid/20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a,json@patachina,0,image/jpeg,1257412,4032,3024,90,1503857624000
80,37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687,aves-remote://rid/37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687,json@patachina,0,image/jpeg,2405533,4032,3024,90,1503862949000
81,1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7,aves-remote://rid/1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7,json@patachina,0,image/jpeg,2066043,4032,3024,90,1503909783000
82,6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b,aves-remote://rid/6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b,json@patachina,0,image/jpeg,4708675,4032,3024,90,1503916727000
83,6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc,aves-remote://rid/6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc,json@patachina,0,image/jpeg,4888844,4032,3024,90,1503916733000
84,e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2,aves-remote://rid/e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2,json@patachina,0,image/jpeg,4413193,4032,3024,90,1503916750000
85,9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b,aves-remote://rid/9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b,json@patachina,0,image/jpeg,3314673,3630,3790,270,1503916764000
86,3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27,aves-remote://rid/3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27,json@patachina,0,image/jpeg,1745323,4032,3024,0,1504001722000
87,b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc,aves-remote://rid/b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc,json@patachina,0,image/jpeg,1242828,4032,3024,0,1504005466000
88,b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9,aves-remote://rid/b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9,json@patachina,0,image/jpeg,3347118,4032,3024,0,1504005486000
89,b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020,aves-remote://rid/b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020,json@patachina,0,image/jpeg,4832084,4032,3024,90,1504006676000
90,e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186,aves-remote://rid/e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186,json@patachina,0,image/jpeg,4210606,4032,3024,90,1504006732000
91,b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751,aves-remote://rid/b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751,json@patachina,0,image/jpeg,4047020,4032,3024,0,1504006811000
92,d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030,aves-remote://rid/d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030,json@patachina,0,image/jpeg,3155785,4032,3024,90,1504006896000
93,0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763,aves-remote://rid/0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763,json@patachina,0,image/jpeg,4551844,4032,3024,90,1504007112000
94,10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457,aves-remote://rid/10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457,json@patachina,0,image/jpeg,4772193,4032,3024,90,1504007140000
95,a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e,aves-remote://rid/a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e,json@patachina,0,image/jpeg,1840010,4000,2250,90,1622628398000
96,5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72,aves-remote://rid/5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72,json@patachina,0,image/jpeg,1756574,4000,2250,0,1622655546000
97,283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1,aves-remote://rid/283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1,json@patachina,0,image/jpeg,4410362,2592,4608,0,1655646943000
98,11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1,aves-remote://rid/11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1,json@patachina,0,image/jpeg,4473038,2592,4608,0,1655647001000
99,87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968,aves-remote://rid/87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968,json@patachina,0,image/jpeg,4782460,2592,4608,0,1655647064000
100,f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488,aves-remote://rid/f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488,json@patachina,0,image/jpeg,4749840,2592,4608,0,1655647065000
101,e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1,aves-remote://rid/e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1,json@patachina,0,video/mp4,12273606,1920,1080,,1773491615180
1 id remoteId uri provider trashed sourceMimeType sizeBytes remoteWidth remoteHeight remoteRotation dateModifiedMillis
2 1 b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e aves-remote://rid/b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e json@patachina 0 image/jpeg 3758888 4032 3024 0 1503481740000
3 2 09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b aves-remote://rid/09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b json@patachina 0 image/jpeg 3984165 4032 3024 0 1503493700000
4 3 dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7 aves-remote://rid/dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7 json@patachina 0 image/jpeg 4338120 5326 3950 0 1503496121000
5 4 751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0 aves-remote://rid/751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0 json@patachina 0 image/jpeg 3159006 4032 3024 0 1503496785000
6 5 73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937 aves-remote://rid/73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937 json@patachina 0 image/jpeg 3338305 4032 3024 0 1503496787000
7 6 f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8 aves-remote://rid/f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8 json@patachina 0 image/jpeg 2815021 4032 3024 0 1503496997000
8 7 90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe aves-remote://rid/90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe json@patachina 0 image/jpeg 3207260 4032 3024 0 1503497854000
9 8 4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935 aves-remote://rid/4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935 json@patachina 0 image/jpeg 2674913 4032 3024 0 1503497873000
10 9 1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f aves-remote://rid/1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f json@patachina 0 image/jpeg 2788605 4032 3024 0 1503498970000
11 10 14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2 aves-remote://rid/14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2 json@patachina 0 image/jpeg 3447883 4032 3024 90 1503504834000
12 11 7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41 aves-remote://rid/7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41 json@patachina 0 image/jpeg 3513469 4032 3024 0 1503505103000
13 12 4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c aves-remote://rid/4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c json@patachina 0 image/jpeg 2353262 4032 3024 0 1503509034000
14 13 490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750 aves-remote://rid/490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750 json@patachina 0 image/jpeg 2912549 4032 3024 90 1503510106000
15 14 1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be aves-remote://rid/1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be json@patachina 0 image/jpeg 2351855 4032 3024 90 1503510129000
16 15 486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3 aves-remote://rid/486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3 json@patachina 0 image/jpeg 1869483 4032 3024 0 1503512829000
17 16 69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a aves-remote://rid/69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a json@patachina 0 image/jpeg 1800632 4032 3024 90 1503512842000
18 17 1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2 aves-remote://rid/1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2 json@patachina 0 image/jpeg 1643315 4032 3024 90 1503514391000
19 18 7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a aves-remote://rid/7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a json@patachina 0 image/jpeg 1450013 4032 3024 90 1503514403000
20 19 35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac aves-remote://rid/35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac json@patachina 0 image/jpeg 1606095 4032 3024 90 1503514433000
21 20 48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274 aves-remote://rid/48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274 json@patachina 0 image/jpeg 1839090 4032 3024 90 1503518935000
22 21 10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134 aves-remote://rid/10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134 json@patachina 0 image/jpeg 2013689 4032 3024 90 1503519976000
23 22 c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969 aves-remote://rid/c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969 json@patachina 0 image/jpeg 1557955 3724 2096 90 1773491614087
24 23 1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90 aves-remote://rid/1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90 json@patachina 0 image/jpeg 2667599 4032 3024 0 1503563815000
25 24 bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0 aves-remote://rid/bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0 json@patachina 0 image/jpeg 2698118 4032 3024 0 1503563819000
26 25 7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f aves-remote://rid/7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f json@patachina 0 image/jpeg 2656457 4032 3024 0 1503563838000
27 26 29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b aves-remote://rid/29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b json@patachina 0 image/jpeg 4654237 4032 3024 90 1503583782000
28 27 ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839 aves-remote://rid/ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839 json@patachina 0 image/jpeg 3760564 4032 3024 0 1503584553000
29 28 c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6 aves-remote://rid/c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6 json@patachina 0 image/jpeg 3627847 4032 3024 0 1503594928000
30 29 f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f aves-remote://rid/f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f json@patachina 0 image/jpeg 1900418 4032 3024 90 1503605922000
31 30 5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e aves-remote://rid/5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e json@patachina 0 image/jpeg 1944490 4032 3024 90 1503605929000
32 31 dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c aves-remote://rid/dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c json@patachina 0 image/jpeg 2020943 4032 3024 90 1503606518000
33 32 8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8 aves-remote://rid/8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8 json@patachina 0 image/jpeg 2632829 4032 3024 90 1503656054000
34 33 46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc aves-remote://rid/46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc json@patachina 0 image/jpeg 3300375 4032 3024 0 1503656159000
35 34 999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e aves-remote://rid/999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e json@patachina 0 image/jpeg 4461504 5532 3898 0 1503656554000
36 35 7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436 aves-remote://rid/7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436 json@patachina 0 image/jpeg 4964535 5232 3872 270 1503656644000
37 36 4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69 aves-remote://rid/4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69 json@patachina 0 image/jpeg 4837273 5300 3908 270 1503656673000
38 37 d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4 aves-remote://rid/d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4 json@patachina 0 image/jpeg 1697485 4032 3024 0 1503656711000
39 38 49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600 aves-remote://rid/49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600 json@patachina 0 image/jpeg 1680963 4032 3024 90 1503656725000
40 39 a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7 aves-remote://rid/a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7 json@patachina 0 image/jpeg 1502040 3024 4032 1503656771000
41 40 fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6 aves-remote://rid/fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6 json@patachina 0 image/jpeg 1520788 4032 3024 90 1503656843000
42 41 c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93 aves-remote://rid/c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93 json@patachina 0 image/jpeg 4954269 8178 3754 0 1503657571000
43 42 7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d aves-remote://rid/7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d json@patachina 0 image/jpeg 2747267 4032 3024 90 1503666106000
44 43 23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f aves-remote://rid/23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f json@patachina 0 image/jpeg 2808395 4032 3024 0 1503666149000
45 44 0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843 aves-remote://rid/0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843 json@patachina 0 image/jpeg 3137643 4032 3024 0 1503666179000
46 45 64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f aves-remote://rid/64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f json@patachina 0 image/jpeg 2929880 4032 3024 90 1503667517000
47 46 8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39 aves-remote://rid/8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39 json@patachina 0 image/jpeg 2498451 4032 3024 90 1503667524000
48 47 ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8 aves-remote://rid/ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8 json@patachina 0 image/jpeg 3020402 4032 3024 90 1503669348000
49 48 a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a aves-remote://rid/a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a json@patachina 0 image/jpeg 2161605 4032 3024 90 1503671202000
50 49 7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a aves-remote://rid/7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a json@patachina 0 image/jpeg 2913137 4032 3024 90 1503686951000
51 50 9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3 aves-remote://rid/9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3 json@patachina 0 image/jpeg 3437757 4032 3024 90 1503686966000
52 51 df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a aves-remote://rid/df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a json@patachina 0 image/jpeg 1668522 3724 2096 90 1773491614875
53 52 f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6 aves-remote://rid/f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6 json@patachina 0 image/jpeg 1781263 3724 2096 90 1773491614881
54 53 68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56 aves-remote://rid/68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56 json@patachina 0 image/jpeg 973684 3763 1657 1503695558000
55 54 6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4 aves-remote://rid/6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4 json@patachina 0 image/jpeg 1311890 3724 2096 0 1773491614903
56 55 dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66 aves-remote://rid/dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66 json@patachina 0 image/jpeg 4612510 7506 3870 0 1503734480000
57 56 4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf aves-remote://rid/4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf json@patachina 0 image/jpeg 4484747 5624 3958 0 1503734933000
58 57 4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc aves-remote://rid/4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc json@patachina 0 image/jpeg 2873334 8968 1560 1503743055000
59 58 0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378 aves-remote://rid/0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378 json@patachina 0 image/jpeg 2168733 4032 3024 90 1503749114000
60 59 4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836 aves-remote://rid/4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836 json@patachina 0 image/jpeg 1891230 4032 3024 90 1503749116000
61 60 76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984 aves-remote://rid/76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984 json@patachina 0 image/jpeg 1766923 4032 3024 90 1503749303000
62 61 065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd aves-remote://rid/065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd json@patachina 0 image/jpeg 2935053 4032 3024 0 1503767620000
63 62 e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed aves-remote://rid/e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed json@patachina 0 image/jpeg 1852261 4032 3024 0 1503767749000
64 63 c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f aves-remote://rid/c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f json@patachina 0 image/jpeg 1936144 4032 3024 0 1503767754000
65 64 77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd aves-remote://rid/77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd json@patachina 0 image/jpeg 1843400 4032 3024 0 1503768183000
66 65 be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7 aves-remote://rid/be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7 json@patachina 0 image/jpeg 4221357 4658 3974 0 1503769227000
67 66 6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7 aves-remote://rid/6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7 json@patachina 0 image/jpeg 4179034 7305 2311 1503769265000
68 67 ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db aves-remote://rid/ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db json@patachina 0 image/jpeg 1750381 1680 4030 0 1503769375000
69 68 3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf aves-remote://rid/3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf json@patachina 0 image/jpeg 2046548 4427 2389 1503769385000
70 69 3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722 aves-remote://rid/3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722 json@patachina 0 image/jpeg 4328104 6438 3900 1503769503000
71 70 50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027 aves-remote://rid/50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027 json@patachina 0 image/jpeg 703594 3946 2960 1503779894000
72 71 df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e aves-remote://rid/df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e json@patachina 0 image/jpeg 468928 3745 1941 1503779936000
73 72 2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070 aves-remote://rid/2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070 json@patachina 0 image/jpeg 2097953 4032 3024 90 1503822184000
74 73 bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba aves-remote://rid/bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba json@patachina 0 image/jpeg 2489649 4032 3024 0 1503823090000
75 74 a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9 aves-remote://rid/a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9 json@patachina 0 image/jpeg 3009764 4032 3024 0 1503830851000
76 75 915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f aves-remote://rid/915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f json@patachina 0 image/jpeg 2757600 4032 3024 0 1503830943000
77 76 0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82 aves-remote://rid/0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82 json@patachina 0 image/jpeg 3442902 4032 3024 0 1503830950000
78 77 a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9 aves-remote://rid/a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9 json@patachina 0 image/jpeg 3891743 4032 3024 90 1503833355000
79 78 308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df aves-remote://rid/308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df json@patachina 0 image/jpeg 1551495 3162 1743 1503844208000
80 79 20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a aves-remote://rid/20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a json@patachina 0 image/jpeg 1257412 4032 3024 90 1503857624000
81 80 37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687 aves-remote://rid/37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687 json@patachina 0 image/jpeg 2405533 4032 3024 90 1503862949000
82 81 1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7 aves-remote://rid/1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7 json@patachina 0 image/jpeg 2066043 4032 3024 90 1503909783000
83 82 6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b aves-remote://rid/6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b json@patachina 0 image/jpeg 4708675 4032 3024 90 1503916727000
84 83 6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc aves-remote://rid/6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc json@patachina 0 image/jpeg 4888844 4032 3024 90 1503916733000
85 84 e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2 aves-remote://rid/e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2 json@patachina 0 image/jpeg 4413193 4032 3024 90 1503916750000
86 85 9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b aves-remote://rid/9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b json@patachina 0 image/jpeg 3314673 3630 3790 270 1503916764000
87 86 3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27 aves-remote://rid/3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27 json@patachina 0 image/jpeg 1745323 4032 3024 0 1504001722000
88 87 b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc aves-remote://rid/b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc json@patachina 0 image/jpeg 1242828 4032 3024 0 1504005466000
89 88 b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9 aves-remote://rid/b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9 json@patachina 0 image/jpeg 3347118 4032 3024 0 1504005486000
90 89 b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020 aves-remote://rid/b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020 json@patachina 0 image/jpeg 4832084 4032 3024 90 1504006676000
91 90 e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186 aves-remote://rid/e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186 json@patachina 0 image/jpeg 4210606 4032 3024 90 1504006732000
92 91 b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751 aves-remote://rid/b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751 json@patachina 0 image/jpeg 4047020 4032 3024 0 1504006811000
93 92 d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030 aves-remote://rid/d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030 json@patachina 0 image/jpeg 3155785 4032 3024 90 1504006896000
94 93 0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763 aves-remote://rid/0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763 json@patachina 0 image/jpeg 4551844 4032 3024 90 1504007112000
95 94 10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457 aves-remote://rid/10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457 json@patachina 0 image/jpeg 4772193 4032 3024 90 1504007140000
96 95 a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e aves-remote://rid/a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e json@patachina 0 image/jpeg 1840010 4000 2250 90 1622628398000
97 96 5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72 aves-remote://rid/5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72 json@patachina 0 image/jpeg 1756574 4000 2250 0 1622655546000
98 97 283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 aves-remote://rid/283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 json@patachina 0 image/jpeg 4410362 2592 4608 0 1655646943000
99 98 11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 aves-remote://rid/11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 json@patachina 0 image/jpeg 4473038 2592 4608 0 1655647001000
100 99 87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 aves-remote://rid/87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 json@patachina 0 image/jpeg 4782460 2592 4608 0 1655647064000
101 100 f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 aves-remote://rid/f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 json@patachina 0 image/jpeg 4749840 2592 4608 0 1655647065000
102 101 e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1 aves-remote://rid/e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1 json@patachina 0 video/mp4 12273606 1920 1080 1773491615180

500
after_commit_500.txt Normal file
View file

@ -0,0 +1,500 @@
I/flutter ( 2891): [repo-upsert] batch.commit resultsCount=101
I/flutter ( 2891): [RemoteRepository] deduplicateRemotes deleted=0
I/flutter ( 2891): [RemoteRepository] deduplicateByRemotePath deleted=0
I/flutter ( 2891): [RemoteRepository] ensured UNIQUE index on entry(remoteId) for origin=1
I/flutter ( 2891): [RemoteRepository] ensured UNIQUE index on entry(remotePath) for origin=1
I/flutter ( 2891): [RemoteRepository] backfill URIs via SQL (remoteId) updated=0
I/flutter ( 2891): [RemoteRepository] backfill dateModifiedMillis updated=0
I/flutter ( 2891): [remote-sync] upsert+sanitize completed in 698ms
I/flutter ( 2891): [remote-sync][post] count origin=1 = 101
I/flutter ( 2891): [remote-sync][post] sample origin=1 = [{id: 101, remoteId: e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1, provider: json@patachina, uri: aves-remote://rid/e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1, trashed: 0, dateModifiedMillis: 1773494703526}, {id: 100, remoteId: f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488, provider: json@patachina, uri: aves-remote://rid/f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488, trashed: 0, dateModifiedMillis: 1655647065000}, {id: 99, remoteId: 87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968, provider: json@patachina, uri: aves-remote://rid/87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968, trashed: 0, dateModifiedMillis: 1655647064000}]
I/flutter ( 2891): [remote-sync] completed in 3816ms, imported=101
I/flutter ( 2891): [remote-sync] END (active=false)
D/OpenGLRenderer( 2891): --- Failed to create image decoder with message 'unimplemented'
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): MediaStoreSource load 0:00:07.563084 save 6956 new entries
I/flutter ( 2891): SqfliteLocalMediaDb saveEntries inserting slice of [1, 6956] entries
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): SqfliteLocalMediaDb saveEntries complete in 737ms for 6956 entries
I/flutter ( 2891): MediaStoreSource load 0:00:08.864527 analyze
I/flutter ( 2891): analyze 6956 entries, force=false, starting service=false
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=requestGarbageCollection arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/ault.aves.debug( 2891): Explicit concurrent copying GC freed 21MB AllocSpace bytes, 3(132KB) LOS objects, 73% free, 8648KB/32MB, paused 31us,24us total 24.653ms
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000020101, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2026-03-12-13-28-00-079_deckers.thibault.aves.debug.jpg, sizeBytes: 780252}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000019657, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2025-10-27-21-11-59-581_com.example.webauthn_flutter.jpg, sizeBytes: 184918}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000019251, path: /storage/emulated/0/DCIM/Camera/IMG_20250810_121828.jpg, sizeBytes: 6177515}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000019250, path: /storage/emulated/0/DCIM/Camera/IMG_20250810_121822.jpg, sizeBytes: 3773772}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000019249, path: /storage/emulated/0/DCIM/Camera/IMG_20250810_121817.jpg, sizeBytes: 4270621}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000018527, path: /storage/emulated/0/DCIM/Camera/IMG_20250204_104544.jpg, sizeBytes: 6799176}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000018526, path: /storage/emulated/0/DCIM/Camera/IMG_20250204_104528.jpg, sizeBytes: 4551647}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000018523, path: /storage/emulated/0/DCIM/Camera/IMG_20250129_080657.jpg, sizeBytes: 4161293}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000018486, path: /storage/emulated/0/DCIM/Camera/IMG_20250125_103858.jpg, sizeBytes: 4403217}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000018374, path: /storage/emulated/0/Pictures/IMG_0206.jpg, sizeBytes: 268463}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017561, path: /storage/emulated/0/DCIM/Camera/IMG_20230406_093739.jpg, sizeBytes: 4581800}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017199, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0023.jpg, sizeBytes: 100057}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017196, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0022.jpg, sizeBytes: 102459}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017192, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0021.jpg, sizeBytes: 150438}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017190, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-15-14-56-56-555_com.google.android.apps.docs-edit.jpg, sizeBytes: 289800}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017189, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-15-14-56-56-555_com.google.android.apps.docs.jpg, sizeBytes: 654556}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017176, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0019.jpg, sizeBytes: 254051}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017173, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0018.jpg, sizeBytes: 234955}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017168, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0017.jpg, sizeBytes: 417289}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017162, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0015.jpg, sizeBytes: 480560}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017165, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0016.jpg, sizeBytes: 365348}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017156, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0013.jpg, sizeBytes: 261621}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017159, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0014.jpg, sizeBytes: 343535}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017150, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0011.jpg, sizeBytes: 224870}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017153, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0012.jpg, sizeBytes: 250785}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017147, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0010.jpg, sizeBytes: 250725}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017140, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0009.jpg, sizeBytes: 151431}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017137, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0008.jpg, sizeBytes: 280768}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017134, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0007.jpg, sizeBytes: 186948}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017131, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0006.jpg, sizeBytes: 643220}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017128, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0005.jpg, sizeBytes: 280447}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017121, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0004.jpg, sizeBytes: 169269}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017115, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0003.jpg, sizeBytes: 133330}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017112, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0002.jpg, sizeBytes: 167765}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017109, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0001.jpg, sizeBytes: 246704}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017099, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230215-WA0000.jpg, sizeBytes: 121328}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017096, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0010.jpg, sizeBytes: 203460}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017087, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0008.jpg, sizeBytes: 56841}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017081, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0006.jpg, sizeBytes: 61761}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017084, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0007.jpg, sizeBytes: 56259}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017078, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0005.jpg, sizeBytes: 104603}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017072, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0004.jpg, sizeBytes: 149717}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017070, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0003.jpg, sizeBytes: 169100}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017068, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0002.jpg, sizeBytes: 113876}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Cataloguing, message: 43/6956}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017065, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0001.jpg, sizeBytes: 67796}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017062, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230214-WA0000.jpg, sizeBytes: 101131}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017052, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230213-WA0004.jpg, sizeBytes: 194676}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017049, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230213-WA0003.jpg, sizeBytes: 310678}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017046, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230213-WA0002.jpg, sizeBytes: 218853}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017044, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-13-12-08-26-323_com.miui.home.jpg, sizeBytes: 794984}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017041, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230213-WA0000.jpg, sizeBytes: 39706}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017040, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-13-10-07-09-365_com.miui.home.jpg, sizeBytes: 807932}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017015, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230212-WA0005.jpg, sizeBytes: 615150}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017008, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230212-WA0004.jpg, sizeBytes: 115959}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017006, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-12-14-17-21-917_com.instagram.android.jpg, sizeBytes: 801141}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000017003, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230212-WA0002.jpg, sizeBytes: 266459}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016999, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-12-08-21-46-596_com.whatsapp.jpg, sizeBytes: 876809}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016989, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230211-WA0014.jpg, sizeBytes: 106154}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016973, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-11-18-05-12-024_com.google.android.gm.jpg, sizeBytes: 615455}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016971, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-11-17-33-02-236_com.whatsapp.jpg, sizeBytes: 828351}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016968, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230211-WA0009.jpg, sizeBytes: 60850}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016965, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230211-WA0008.jpg, sizeBytes: 74140}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016960, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-11-12-29-01-111_com.whatsapp.jpg, sizeBytes: 828986}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016958, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-11-12-28-39-912_com.whatsapp.jpg, sizeBytes: 724062}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016945, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230211-WA0001.jpg, sizeBytes: 271535}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016942, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230211-WA0000.jpg, sizeBytes: 126235}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016926, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230210-WA0006.jpg, sizeBytes: 137403}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016924, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-10-17-31-10-634_com.google.android.googlequicksearchbox-edit.jpg, sizeBytes: 272112}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016922, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230209-WA0004.jpg, sizeBytes: 129533}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016920, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-10-15-57-09-170_com.miui.home.jpg, sizeBytes: 625585}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016917, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230210-WA0003.jpg, sizeBytes: 64094}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016914, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230210-WA0002.jpg, sizeBytes: 318779}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016911, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230210-WA0001.jpg, sizeBytes: 42122}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016890, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230209-WA0014.jpg, sizeBytes: 155891}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016876, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230209-WA0011.jpg, sizeBytes: 192046}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016851, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230209-WA0006.jpg, sizeBytes: 67535}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016812, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0010.jpg, sizeBytes: 371772}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016806, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0008.jpg, sizeBytes: 382274}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016809, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0009.jpg, sizeBytes: 439268}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016798, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0006.jpg, sizeBytes: 422064}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016795, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0005.jpg, sizeBytes: 338082}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016789, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0003.jpg, sizeBytes: 279930}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016792, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0004.jpg, sizeBytes: 190775}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016786, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0002.jpg, sizeBytes: 289841}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016778, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230208-WA0000.jpg, sizeBytes: 409315}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016765, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230207-WA0001.jpg, sizeBytes: 216388}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016762, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230207-WA0000.jpg, sizeBytes: 213350}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016738, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-07-16-13-05-638_com.shazam.android.jpg, sizeBytes: 941691}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016707, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230206-WA0000.jpg, sizeBytes: 193385}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016692, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0032.jpg, sizeBytes: 215636}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016689, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0031.jpg, sizeBytes: 279947}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016686, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0030.jpg, sizeBytes: 331013}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016683, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0029.jpg, sizeBytes: 328680}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016680, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0028.jpg, sizeBytes: 337417}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016677, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0027.jpg, sizeBytes: 294504}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016674, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0026.jpg, sizeBytes: 358036}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016668, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0024.jpg, sizeBytes: 333838}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016671, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0025.jpg, sizeBytes: 347022}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016665, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0023.jpg, sizeBytes: 324751}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016659, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0021.jpg, sizeBytes: 240105}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016662, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0022.jpg, sizeBytes: 353403}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016656, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0020.jpg, sizeBytes: 382894}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016653, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0019.jpg, sizeBytes: 385053}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016650, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0018.jpg, sizeBytes: 398987}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016599, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0002.jpg, sizeBytes: 340368}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016646, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0017.jpg, sizeBytes: 330573}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016643, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0016.jpg, sizeBytes: 391092}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016640, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0015.jpg, sizeBytes: 354548}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016634, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0013.jpg, sizeBytes: 386698}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016637, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0014.jpg, sizeBytes: 370078}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016628, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0011.jpg, sizeBytes: 370664}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016631, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0012.jpg, sizeBytes: 280670}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016622, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0009.jpg, sizeBytes: 318984}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016625, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0010.jpg, sizeBytes: 302076}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016619, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0008.jpg, sizeBytes: 339385}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016613, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0006.jpg, sizeBytes: 341430}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016616, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0007.jpg, sizeBytes: 348511}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016610, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0005.jpg, sizeBytes: 351561}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016607, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0004.jpg, sizeBytes: 361860}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016596, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230205-WA0001.jpg, sizeBytes: 115084}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016586, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-04-17-49-15-733_com.android.chrome.jpg, sizeBytes: 621329}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016585, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-04-17-48-21-380_com.android.chrome.jpg, sizeBytes: 746568}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016584, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-04-17-48-15-501_com.android.chrome.jpg, sizeBytes: 618906}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Cataloguing, message: 123/6956}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016581, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-04-17-15-21-802_com.google.android.googlequicksearchbox.jpg, sizeBytes: 605874}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016572, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230204-WA0000.jpg, sizeBytes: 193943}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016545, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-03-19-56-29-319_com.whatsapp.jpg, sizeBytes: 836985}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016536, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230203-WA0003.jpg, sizeBytes: 345525}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016539, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230203-WA0004.jpg, sizeBytes: 501543}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016533, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230203-WA0002.jpg, sizeBytes: 591603}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016527, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230203-WA0001.jpg, sizeBytes: 143731}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016524, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230203-WA0000.jpg, sizeBytes: 85831}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016513, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230202-WA0004.jpg, sizeBytes: 136481}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016511, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-02-18-13-19-852_ch.publisheria.bring.jpg, sizeBytes: 496941}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016505, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-02-02-16-34-54-238_com.google.android.googlequicksearchbox-edit.jpg, sizeBytes: 588438}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016497, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230202-WA0000.jpg, sizeBytes: 430669}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016477, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230201-WA0003.jpg, sizeBytes: 395349}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016474, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230201-WA0002.jpg, sizeBytes: 605187}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016470, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230201-WA0000.jpg, sizeBytes: 197092}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016454, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230131-WA0006.jpg, sizeBytes: 260899}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016451, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230131-WA0005.jpg, sizeBytes: 228335}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016449, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-01-31-12-21-22-297_com.momondo.flightsearch.jpg, sizeBytes: 557143}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016447, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-01-31-12-10-40-642_com.momondo.flightsearch.jpg, sizeBytes: 563187}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016446, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-01-31-12-10-30-976_com.momondo.flightsearch.jpg, sizeBytes: 562512}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016444, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-01-31-10-54-37-729_com.google.android.gm.jpg, sizeBytes: 616113}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016430, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230130-WA0007.jpg, sizeBytes: 224338}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016412, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230130-WA0005.jpg, sizeBytes: 135044}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016409, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230130-WA0004.jpg, sizeBytes: 38680}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016402, path: /storage/emulated/0/DCIM/Screenshots/Screenshot_2023-01-30-12-01-39-334_com.google.android.gm.jpg, sizeBytes: 329317}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016399, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230130-WA0000.jpg, sizeBytes: 307208}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016388, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230129-WA0000.jpg, sizeBytes: 141167}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016361, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230128-WA0014.jpg, sizeBytes: 65570}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/metadata_fetch method=getCatalogMetadata arguments={mimeType: image/jpeg, uri: content://media/external/images/media/1000016358, path: /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Images/IMG-20230128-WA0013.jpg, sizeBytes: 98011}
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/device method=getAvailableHeapSize arguments=null

2
android_metadata.csv Normal file
View file

@ -0,0 +1,2 @@
locale
it_IT
1 locale
2 it_IT

1
apk.sh Normal file
View file

@ -0,0 +1 @@
fvm flutter build apk --debug -t lib/main_play.dart --flavor play

102
before.csv Normal file
View file

@ -0,0 +1,102 @@
id,remoteId,uri,provider,trashed,sourceMimeType,sizeBytes,remoteWidth,remoteHeight,remoteRotation,dateModifiedMillis
1,b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e,aves-remote://rid/b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e,json@patachina,0,image/jpeg,3758888,4032,3024,0,1503481740000
2,09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b,aves-remote://rid/09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b,json@patachina,0,image/jpeg,3984165,4032,3024,0,1503493700000
3,dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7,aves-remote://rid/dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7,json@patachina,0,image/jpeg,4338120,5326,3950,0,1503496121000
4,751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0,aves-remote://rid/751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0,json@patachina,0,image/jpeg,3159006,4032,3024,0,1503496785000
5,73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937,aves-remote://rid/73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937,json@patachina,0,image/jpeg,3338305,4032,3024,0,1503496787000
6,f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8,aves-remote://rid/f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8,json@patachina,0,image/jpeg,2815021,4032,3024,0,1503496997000
7,90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe,aves-remote://rid/90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe,json@patachina,0,image/jpeg,3207260,4032,3024,0,1503497854000
8,4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935,aves-remote://rid/4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935,json@patachina,0,image/jpeg,2674913,4032,3024,0,1503497873000
9,1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f,aves-remote://rid/1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f,json@patachina,0,image/jpeg,2788605,4032,3024,0,1503498970000
10,14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2,aves-remote://rid/14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2,json@patachina,0,image/jpeg,3447883,4032,3024,90,1503504834000
11,7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41,aves-remote://rid/7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41,json@patachina,0,image/jpeg,3513469,4032,3024,0,1503505103000
12,4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c,aves-remote://rid/4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c,json@patachina,0,image/jpeg,2353262,4032,3024,0,1503509034000
13,490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750,aves-remote://rid/490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750,json@patachina,0,image/jpeg,2912549,4032,3024,90,1503510106000
14,1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be,aves-remote://rid/1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be,json@patachina,0,image/jpeg,2351855,4032,3024,90,1503510129000
15,486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3,aves-remote://rid/486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3,json@patachina,0,image/jpeg,1869483,4032,3024,0,1503512829000
16,69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a,aves-remote://rid/69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a,json@patachina,0,image/jpeg,1800632,4032,3024,90,1503512842000
17,1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2,aves-remote://rid/1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2,json@patachina,0,image/jpeg,1643315,4032,3024,90,1503514391000
18,7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a,aves-remote://rid/7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a,json@patachina,0,image/jpeg,1450013,4032,3024,90,1503514403000
19,35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac,aves-remote://rid/35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac,json@patachina,0,image/jpeg,1606095,4032,3024,90,1503514433000
20,48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274,aves-remote://rid/48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274,json@patachina,0,image/jpeg,1839090,4032,3024,90,1503518935000
21,10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134,aves-remote://rid/10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134,json@patachina,0,image/jpeg,2013689,4032,3024,90,1503519976000
22,c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969,aves-remote://rid/c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969,json@patachina,0,image/jpeg,1557955,3724,2096,90,1773490241586
23,1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90,aves-remote://rid/1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90,json@patachina,0,image/jpeg,2667599,4032,3024,0,1503563815000
24,bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0,aves-remote://rid/bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0,json@patachina,0,image/jpeg,2698118,4032,3024,0,1503563819000
25,7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f,aves-remote://rid/7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f,json@patachina,0,image/jpeg,2656457,4032,3024,0,1503563838000
26,29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b,aves-remote://rid/29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b,json@patachina,0,image/jpeg,4654237,4032,3024,90,1503583782000
27,ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839,aves-remote://rid/ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839,json@patachina,0,image/jpeg,3760564,4032,3024,0,1503584553000
28,c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6,aves-remote://rid/c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6,json@patachina,0,image/jpeg,3627847,4032,3024,0,1503594928000
29,f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f,aves-remote://rid/f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f,json@patachina,0,image/jpeg,1900418,4032,3024,90,1503605922000
30,5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e,aves-remote://rid/5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e,json@patachina,0,image/jpeg,1944490,4032,3024,90,1503605929000
31,dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c,aves-remote://rid/dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c,json@patachina,0,image/jpeg,2020943,4032,3024,90,1503606518000
32,8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8,aves-remote://rid/8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8,json@patachina,0,image/jpeg,2632829,4032,3024,90,1503656054000
33,46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc,aves-remote://rid/46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc,json@patachina,0,image/jpeg,3300375,4032,3024,0,1503656159000
34,999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e,aves-remote://rid/999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e,json@patachina,0,image/jpeg,4461504,5532,3898,0,1503656554000
35,7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436,aves-remote://rid/7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436,json@patachina,0,image/jpeg,4964535,5232,3872,270,1503656644000
36,4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69,aves-remote://rid/4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69,json@patachina,0,image/jpeg,4837273,5300,3908,270,1503656673000
37,d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4,aves-remote://rid/d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4,json@patachina,0,image/jpeg,1697485,4032,3024,0,1503656711000
38,49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600,aves-remote://rid/49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600,json@patachina,0,image/jpeg,1680963,4032,3024,90,1503656725000
39,a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7,aves-remote://rid/a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7,json@patachina,0,image/jpeg,1502040,3024,4032,,1503656771000
40,fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6,aves-remote://rid/fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6,json@patachina,0,image/jpeg,1520788,4032,3024,90,1503656843000
41,c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93,aves-remote://rid/c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93,json@patachina,0,image/jpeg,4954269,8178,3754,0,1503657571000
42,7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d,aves-remote://rid/7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d,json@patachina,0,image/jpeg,2747267,4032,3024,90,1503666106000
43,23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f,aves-remote://rid/23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f,json@patachina,0,image/jpeg,2808395,4032,3024,0,1503666149000
44,0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843,aves-remote://rid/0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843,json@patachina,0,image/jpeg,3137643,4032,3024,0,1503666179000
45,64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f,aves-remote://rid/64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f,json@patachina,0,image/jpeg,2929880,4032,3024,90,1503667517000
46,8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39,aves-remote://rid/8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39,json@patachina,0,image/jpeg,2498451,4032,3024,90,1503667524000
47,ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8,aves-remote://rid/ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8,json@patachina,0,image/jpeg,3020402,4032,3024,90,1503669348000
48,a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a,aves-remote://rid/a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a,json@patachina,0,image/jpeg,2161605,4032,3024,90,1503671202000
49,7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a,aves-remote://rid/7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a,json@patachina,0,image/jpeg,2913137,4032,3024,90,1503686951000
50,9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3,aves-remote://rid/9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3,json@patachina,0,image/jpeg,3437757,4032,3024,90,1503686966000
51,df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a,aves-remote://rid/df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a,json@patachina,0,image/jpeg,1668522,3724,2096,90,1773490241712
52,f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6,aves-remote://rid/f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6,json@patachina,0,image/jpeg,1781263,3724,2096,90,1773490241720
53,68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56,aves-remote://rid/68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56,json@patachina,0,image/jpeg,973684,3763,1657,,1503695558000
54,6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4,aves-remote://rid/6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4,json@patachina,0,image/jpeg,1311890,3724,2096,0,1773490241733
55,dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66,aves-remote://rid/dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66,json@patachina,0,image/jpeg,4612510,7506,3870,0,1503734480000
56,4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf,aves-remote://rid/4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf,json@patachina,0,image/jpeg,4484747,5624,3958,0,1503734933000
57,4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc,aves-remote://rid/4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc,json@patachina,0,image/jpeg,2873334,8968,1560,,1503743055000
58,0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378,aves-remote://rid/0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378,json@patachina,0,image/jpeg,2168733,4032,3024,90,1503749114000
59,4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836,aves-remote://rid/4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836,json@patachina,0,image/jpeg,1891230,4032,3024,90,1503749116000
60,76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984,aves-remote://rid/76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984,json@patachina,0,image/jpeg,1766923,4032,3024,90,1503749303000
61,065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd,aves-remote://rid/065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd,json@patachina,0,image/jpeg,2935053,4032,3024,0,1503767620000
62,e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed,aves-remote://rid/e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed,json@patachina,0,image/jpeg,1852261,4032,3024,0,1503767749000
63,c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f,aves-remote://rid/c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f,json@patachina,0,image/jpeg,1936144,4032,3024,0,1503767754000
64,77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd,aves-remote://rid/77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd,json@patachina,0,image/jpeg,1843400,4032,3024,0,1503768183000
65,be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7,aves-remote://rid/be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7,json@patachina,0,image/jpeg,4221357,4658,3974,0,1503769227000
66,6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7,aves-remote://rid/6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7,json@patachina,0,image/jpeg,4179034,7305,2311,,1503769265000
67,ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db,aves-remote://rid/ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db,json@patachina,0,image/jpeg,1750381,1680,4030,0,1503769375000
68,3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf,aves-remote://rid/3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf,json@patachina,0,image/jpeg,2046548,4427,2389,,1503769385000
69,3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722,aves-remote://rid/3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722,json@patachina,0,image/jpeg,4328104,6438,3900,,1503769503000
70,50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027,aves-remote://rid/50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027,json@patachina,0,image/jpeg,703594,3946,2960,,1503779894000
71,df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e,aves-remote://rid/df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e,json@patachina,0,image/jpeg,468928,3745,1941,,1503779936000
72,2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070,aves-remote://rid/2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070,json@patachina,0,image/jpeg,2097953,4032,3024,90,1503822184000
73,bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba,aves-remote://rid/bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba,json@patachina,0,image/jpeg,2489649,4032,3024,0,1503823090000
74,a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9,aves-remote://rid/a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9,json@patachina,0,image/jpeg,3009764,4032,3024,0,1503830851000
75,915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f,aves-remote://rid/915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f,json@patachina,0,image/jpeg,2757600,4032,3024,0,1503830943000
76,0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82,aves-remote://rid/0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82,json@patachina,0,image/jpeg,3442902,4032,3024,0,1503830950000
77,a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9,aves-remote://rid/a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9,json@patachina,0,image/jpeg,3891743,4032,3024,90,1503833355000
78,308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df,aves-remote://rid/308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df,json@patachina,0,image/jpeg,1551495,3162,1743,,1503844208000
79,20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a,aves-remote://rid/20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a,json@patachina,0,image/jpeg,1257412,4032,3024,90,1503857624000
80,37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687,aves-remote://rid/37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687,json@patachina,0,image/jpeg,2405533,4032,3024,90,1503862949000
81,1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7,aves-remote://rid/1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7,json@patachina,0,image/jpeg,2066043,4032,3024,90,1503909783000
82,6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b,aves-remote://rid/6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b,json@patachina,0,image/jpeg,4708675,4032,3024,90,1503916727000
83,6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc,aves-remote://rid/6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc,json@patachina,0,image/jpeg,4888844,4032,3024,90,1503916733000
84,e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2,aves-remote://rid/e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2,json@patachina,0,image/jpeg,4413193,4032,3024,90,1503916750000
85,9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b,aves-remote://rid/9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b,json@patachina,0,image/jpeg,3314673,3630,3790,270,1503916764000
86,3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27,aves-remote://rid/3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27,json@patachina,0,image/jpeg,1745323,4032,3024,0,1504001722000
87,b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc,aves-remote://rid/b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc,json@patachina,0,image/jpeg,1242828,4032,3024,0,1504005466000
88,b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9,aves-remote://rid/b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9,json@patachina,0,image/jpeg,3347118,4032,3024,0,1504005486000
89,b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020,aves-remote://rid/b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020,json@patachina,0,image/jpeg,4832084,4032,3024,90,1504006676000
90,e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186,aves-remote://rid/e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186,json@patachina,0,image/jpeg,4210606,4032,3024,90,1504006732000
91,b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751,aves-remote://rid/b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751,json@patachina,0,image/jpeg,4047020,4032,3024,0,1504006811000
92,d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030,aves-remote://rid/d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030,json@patachina,0,image/jpeg,3155785,4032,3024,90,1504006896000
93,0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763,aves-remote://rid/0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763,json@patachina,0,image/jpeg,4551844,4032,3024,90,1504007112000
94,10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457,aves-remote://rid/10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457,json@patachina,0,image/jpeg,4772193,4032,3024,90,1504007140000
95,a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e,aves-remote://rid/a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e,json@patachina,0,image/jpeg,1840010,4000,2250,90,1622628398000
96,5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72,aves-remote://rid/5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72,json@patachina,0,image/jpeg,1756574,4000,2250,0,1622655546000
97,283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1,aves-remote://rid/283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1,json@patachina,0,image/jpeg,4410362,2592,4608,0,1655646943000
98,11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1,aves-remote://rid/11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1,json@patachina,0,image/jpeg,4473038,2592,4608,0,1655647001000
99,87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968,aves-remote://rid/87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968,json@patachina,0,image/jpeg,4782460,2592,4608,0,1655647064000
100,f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488,aves-remote://rid/f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488,json@patachina,0,image/jpeg,4749840,2592,4608,0,1655647065000
101,e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1,aves-remote://rid/e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1,json@patachina,0,video/mp4,12273606,1920,1080,,1773490244080
1 id remoteId uri provider trashed sourceMimeType sizeBytes remoteWidth remoteHeight remoteRotation dateModifiedMillis
2 1 b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e aves-remote://rid/b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e json@patachina 0 image/jpeg 3758888 4032 3024 0 1503481740000
3 2 09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b aves-remote://rid/09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b json@patachina 0 image/jpeg 3984165 4032 3024 0 1503493700000
4 3 dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7 aves-remote://rid/dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7 json@patachina 0 image/jpeg 4338120 5326 3950 0 1503496121000
5 4 751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0 aves-remote://rid/751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0 json@patachina 0 image/jpeg 3159006 4032 3024 0 1503496785000
6 5 73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937 aves-remote://rid/73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937 json@patachina 0 image/jpeg 3338305 4032 3024 0 1503496787000
7 6 f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8 aves-remote://rid/f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8 json@patachina 0 image/jpeg 2815021 4032 3024 0 1503496997000
8 7 90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe aves-remote://rid/90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe json@patachina 0 image/jpeg 3207260 4032 3024 0 1503497854000
9 8 4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935 aves-remote://rid/4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935 json@patachina 0 image/jpeg 2674913 4032 3024 0 1503497873000
10 9 1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f aves-remote://rid/1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f json@patachina 0 image/jpeg 2788605 4032 3024 0 1503498970000
11 10 14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2 aves-remote://rid/14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2 json@patachina 0 image/jpeg 3447883 4032 3024 90 1503504834000
12 11 7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41 aves-remote://rid/7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41 json@patachina 0 image/jpeg 3513469 4032 3024 0 1503505103000
13 12 4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c aves-remote://rid/4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c json@patachina 0 image/jpeg 2353262 4032 3024 0 1503509034000
14 13 490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750 aves-remote://rid/490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750 json@patachina 0 image/jpeg 2912549 4032 3024 90 1503510106000
15 14 1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be aves-remote://rid/1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be json@patachina 0 image/jpeg 2351855 4032 3024 90 1503510129000
16 15 486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3 aves-remote://rid/486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3 json@patachina 0 image/jpeg 1869483 4032 3024 0 1503512829000
17 16 69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a aves-remote://rid/69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a json@patachina 0 image/jpeg 1800632 4032 3024 90 1503512842000
18 17 1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2 aves-remote://rid/1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2 json@patachina 0 image/jpeg 1643315 4032 3024 90 1503514391000
19 18 7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a aves-remote://rid/7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a json@patachina 0 image/jpeg 1450013 4032 3024 90 1503514403000
20 19 35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac aves-remote://rid/35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac json@patachina 0 image/jpeg 1606095 4032 3024 90 1503514433000
21 20 48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274 aves-remote://rid/48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274 json@patachina 0 image/jpeg 1839090 4032 3024 90 1503518935000
22 21 10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134 aves-remote://rid/10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134 json@patachina 0 image/jpeg 2013689 4032 3024 90 1503519976000
23 22 c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969 aves-remote://rid/c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969 json@patachina 0 image/jpeg 1557955 3724 2096 90 1773490241586
24 23 1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90 aves-remote://rid/1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90 json@patachina 0 image/jpeg 2667599 4032 3024 0 1503563815000
25 24 bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0 aves-remote://rid/bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0 json@patachina 0 image/jpeg 2698118 4032 3024 0 1503563819000
26 25 7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f aves-remote://rid/7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f json@patachina 0 image/jpeg 2656457 4032 3024 0 1503563838000
27 26 29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b aves-remote://rid/29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b json@patachina 0 image/jpeg 4654237 4032 3024 90 1503583782000
28 27 ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839 aves-remote://rid/ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839 json@patachina 0 image/jpeg 3760564 4032 3024 0 1503584553000
29 28 c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6 aves-remote://rid/c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6 json@patachina 0 image/jpeg 3627847 4032 3024 0 1503594928000
30 29 f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f aves-remote://rid/f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f json@patachina 0 image/jpeg 1900418 4032 3024 90 1503605922000
31 30 5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e aves-remote://rid/5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e json@patachina 0 image/jpeg 1944490 4032 3024 90 1503605929000
32 31 dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c aves-remote://rid/dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c json@patachina 0 image/jpeg 2020943 4032 3024 90 1503606518000
33 32 8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8 aves-remote://rid/8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8 json@patachina 0 image/jpeg 2632829 4032 3024 90 1503656054000
34 33 46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc aves-remote://rid/46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc json@patachina 0 image/jpeg 3300375 4032 3024 0 1503656159000
35 34 999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e aves-remote://rid/999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e json@patachina 0 image/jpeg 4461504 5532 3898 0 1503656554000
36 35 7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436 aves-remote://rid/7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436 json@patachina 0 image/jpeg 4964535 5232 3872 270 1503656644000
37 36 4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69 aves-remote://rid/4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69 json@patachina 0 image/jpeg 4837273 5300 3908 270 1503656673000
38 37 d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4 aves-remote://rid/d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4 json@patachina 0 image/jpeg 1697485 4032 3024 0 1503656711000
39 38 49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600 aves-remote://rid/49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600 json@patachina 0 image/jpeg 1680963 4032 3024 90 1503656725000
40 39 a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7 aves-remote://rid/a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7 json@patachina 0 image/jpeg 1502040 3024 4032 1503656771000
41 40 fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6 aves-remote://rid/fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6 json@patachina 0 image/jpeg 1520788 4032 3024 90 1503656843000
42 41 c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93 aves-remote://rid/c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93 json@patachina 0 image/jpeg 4954269 8178 3754 0 1503657571000
43 42 7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d aves-remote://rid/7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d json@patachina 0 image/jpeg 2747267 4032 3024 90 1503666106000
44 43 23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f aves-remote://rid/23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f json@patachina 0 image/jpeg 2808395 4032 3024 0 1503666149000
45 44 0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843 aves-remote://rid/0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843 json@patachina 0 image/jpeg 3137643 4032 3024 0 1503666179000
46 45 64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f aves-remote://rid/64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f json@patachina 0 image/jpeg 2929880 4032 3024 90 1503667517000
47 46 8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39 aves-remote://rid/8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39 json@patachina 0 image/jpeg 2498451 4032 3024 90 1503667524000
48 47 ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8 aves-remote://rid/ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8 json@patachina 0 image/jpeg 3020402 4032 3024 90 1503669348000
49 48 a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a aves-remote://rid/a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a json@patachina 0 image/jpeg 2161605 4032 3024 90 1503671202000
50 49 7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a aves-remote://rid/7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a json@patachina 0 image/jpeg 2913137 4032 3024 90 1503686951000
51 50 9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3 aves-remote://rid/9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3 json@patachina 0 image/jpeg 3437757 4032 3024 90 1503686966000
52 51 df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a aves-remote://rid/df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a json@patachina 0 image/jpeg 1668522 3724 2096 90 1773490241712
53 52 f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6 aves-remote://rid/f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6 json@patachina 0 image/jpeg 1781263 3724 2096 90 1773490241720
54 53 68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56 aves-remote://rid/68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56 json@patachina 0 image/jpeg 973684 3763 1657 1503695558000
55 54 6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4 aves-remote://rid/6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4 json@patachina 0 image/jpeg 1311890 3724 2096 0 1773490241733
56 55 dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66 aves-remote://rid/dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66 json@patachina 0 image/jpeg 4612510 7506 3870 0 1503734480000
57 56 4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf aves-remote://rid/4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf json@patachina 0 image/jpeg 4484747 5624 3958 0 1503734933000
58 57 4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc aves-remote://rid/4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc json@patachina 0 image/jpeg 2873334 8968 1560 1503743055000
59 58 0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378 aves-remote://rid/0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378 json@patachina 0 image/jpeg 2168733 4032 3024 90 1503749114000
60 59 4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836 aves-remote://rid/4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836 json@patachina 0 image/jpeg 1891230 4032 3024 90 1503749116000
61 60 76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984 aves-remote://rid/76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984 json@patachina 0 image/jpeg 1766923 4032 3024 90 1503749303000
62 61 065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd aves-remote://rid/065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd json@patachina 0 image/jpeg 2935053 4032 3024 0 1503767620000
63 62 e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed aves-remote://rid/e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed json@patachina 0 image/jpeg 1852261 4032 3024 0 1503767749000
64 63 c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f aves-remote://rid/c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f json@patachina 0 image/jpeg 1936144 4032 3024 0 1503767754000
65 64 77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd aves-remote://rid/77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd json@patachina 0 image/jpeg 1843400 4032 3024 0 1503768183000
66 65 be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7 aves-remote://rid/be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7 json@patachina 0 image/jpeg 4221357 4658 3974 0 1503769227000
67 66 6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7 aves-remote://rid/6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7 json@patachina 0 image/jpeg 4179034 7305 2311 1503769265000
68 67 ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db aves-remote://rid/ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db json@patachina 0 image/jpeg 1750381 1680 4030 0 1503769375000
69 68 3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf aves-remote://rid/3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf json@patachina 0 image/jpeg 2046548 4427 2389 1503769385000
70 69 3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722 aves-remote://rid/3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722 json@patachina 0 image/jpeg 4328104 6438 3900 1503769503000
71 70 50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027 aves-remote://rid/50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027 json@patachina 0 image/jpeg 703594 3946 2960 1503779894000
72 71 df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e aves-remote://rid/df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e json@patachina 0 image/jpeg 468928 3745 1941 1503779936000
73 72 2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070 aves-remote://rid/2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070 json@patachina 0 image/jpeg 2097953 4032 3024 90 1503822184000
74 73 bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba aves-remote://rid/bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba json@patachina 0 image/jpeg 2489649 4032 3024 0 1503823090000
75 74 a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9 aves-remote://rid/a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9 json@patachina 0 image/jpeg 3009764 4032 3024 0 1503830851000
76 75 915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f aves-remote://rid/915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f json@patachina 0 image/jpeg 2757600 4032 3024 0 1503830943000
77 76 0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82 aves-remote://rid/0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82 json@patachina 0 image/jpeg 3442902 4032 3024 0 1503830950000
78 77 a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9 aves-remote://rid/a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9 json@patachina 0 image/jpeg 3891743 4032 3024 90 1503833355000
79 78 308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df aves-remote://rid/308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df json@patachina 0 image/jpeg 1551495 3162 1743 1503844208000
80 79 20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a aves-remote://rid/20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a json@patachina 0 image/jpeg 1257412 4032 3024 90 1503857624000
81 80 37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687 aves-remote://rid/37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687 json@patachina 0 image/jpeg 2405533 4032 3024 90 1503862949000
82 81 1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7 aves-remote://rid/1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7 json@patachina 0 image/jpeg 2066043 4032 3024 90 1503909783000
83 82 6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b aves-remote://rid/6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b json@patachina 0 image/jpeg 4708675 4032 3024 90 1503916727000
84 83 6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc aves-remote://rid/6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc json@patachina 0 image/jpeg 4888844 4032 3024 90 1503916733000
85 84 e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2 aves-remote://rid/e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2 json@patachina 0 image/jpeg 4413193 4032 3024 90 1503916750000
86 85 9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b aves-remote://rid/9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b json@patachina 0 image/jpeg 3314673 3630 3790 270 1503916764000
87 86 3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27 aves-remote://rid/3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27 json@patachina 0 image/jpeg 1745323 4032 3024 0 1504001722000
88 87 b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc aves-remote://rid/b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc json@patachina 0 image/jpeg 1242828 4032 3024 0 1504005466000
89 88 b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9 aves-remote://rid/b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9 json@patachina 0 image/jpeg 3347118 4032 3024 0 1504005486000
90 89 b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020 aves-remote://rid/b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020 json@patachina 0 image/jpeg 4832084 4032 3024 90 1504006676000
91 90 e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186 aves-remote://rid/e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186 json@patachina 0 image/jpeg 4210606 4032 3024 90 1504006732000
92 91 b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751 aves-remote://rid/b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751 json@patachina 0 image/jpeg 4047020 4032 3024 0 1504006811000
93 92 d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030 aves-remote://rid/d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030 json@patachina 0 image/jpeg 3155785 4032 3024 90 1504006896000
94 93 0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763 aves-remote://rid/0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763 json@patachina 0 image/jpeg 4551844 4032 3024 90 1504007112000
95 94 10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457 aves-remote://rid/10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457 json@patachina 0 image/jpeg 4772193 4032 3024 90 1504007140000
96 95 a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e aves-remote://rid/a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e json@patachina 0 image/jpeg 1840010 4000 2250 90 1622628398000
97 96 5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72 aves-remote://rid/5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72 json@patachina 0 image/jpeg 1756574 4000 2250 0 1622655546000
98 97 283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 aves-remote://rid/283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 json@patachina 0 image/jpeg 4410362 2592 4608 0 1655646943000
99 98 11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 aves-remote://rid/11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 json@patachina 0 image/jpeg 4473038 2592 4608 0 1655647001000
100 99 87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 aves-remote://rid/87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 json@patachina 0 image/jpeg 4782460 2592 4608 0 1655647064000
101 100 f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 aves-remote://rid/f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 json@patachina 0 image/jpeg 4749840 2592 4608 0 1655647065000
102 101 e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1 aves-remote://rid/e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1 json@patachina 0 video/mp4 12273606 1920 1080 1773490244080

1
before.sha1 Normal file
View file

@ -0,0 +1 @@
d293b0e55340b249a24532d2dd61a6ebcfb328fb metadata.db

0
covers.csv Normal file
View file

2401
dateTaken.csv Normal file

File diff suppressed because it is too large Load diff

12
dbcvs.sh Normal file
View file

@ -0,0 +1,12 @@
#!/bin/bash
DB="metadata.db"
for t in $(sqlite3 "$DB" ".tables"); do
echo "Esporto $t..."
sqlite3 "$DB" <<EOF
.headers on
.mode csv
.output ${t}.csv
SELECT * FROM $t;
EOF
done

2
dbdown.sh Normal file
View file

@ -0,0 +1,2 @@
adb exec-out run-as deckers.thibault.aves.debug cat /data/user/0/deckers.thibault.aves.debug/databases/metadata.db > metadata.db
#adb exec-out run-as deckers.thibault.aves.debug cat /data/user/0/deckers.thibault.aves.debug/databases/metadata.db-wal > metadata.db-wal

1
dbnremote.sh Normal file
View file

@ -0,0 +1 @@
sqlite3 -header -csv metadata.db "SELECT COUNT(*) AS nremote FROM entry WHERE origin=1;"

2
dbrecord.sh Normal file
View file

@ -0,0 +1,2 @@
sqlite3 -header -csv metadata.db \ "SELECT * FROM entry WHERE origin=0 ORDER BY id LIMIT 1;"
sqlite3 -header -csv metadata.db \ "SELECT * FROM entry WHERE origin=1 ORDER BY id LIMIT 1;"

7
dbrotate.sh Normal file
View file

@ -0,0 +1,7 @@
sqlite3 metadata.db <<'SQL'
BEGIN;
UPDATE entry SET sourceRotationDegrees = 90 WHERE origin = 1;
COMMIT;
SELECT COUNT(*) AS total_origin_1 FROM entry WHERE origin = 1;
SELECT COUNT(*) AS now_90 FROM entry WHERE origin = 1 AND sourceRotationDegrees = 90;
SQL

7
dbrotate2.sh Normal file
View file

@ -0,0 +1,7 @@
sqlite3 metadata.db <<'SQL'
BEGIN;
UPDATE entry SET sourceRotationDegrees = 0 WHERE origin = 1;
COMMIT;
SELECT COUNT(*) AS total_origin_1 FROM entry WHERE origin = 1;
SELECT COUNT(*) AS now_90 FROM entry WHERE origin = 1 AND sourceRotationDegrees = 0;
SQL

8
dbrotation.sh Normal file
View file

@ -0,0 +1,8 @@
sqlite3 metadata.db <<'SQL'
BEGIN;
UPDATE entry
SET remoteRotation = 0
WHERE origin = 1
AND remoteRotation IS NULL;
COMMIT;
SQL

4
dbupload.sh Normal file
View file

@ -0,0 +1,4 @@
adb push metadata.db /data/local/tmp/metadata.db
adb shell "run-as deckers.thibault.aves.debug sh -c 'cat > /data/user/0/deckers.thibault.aves.debug/databases/metadata.db' < /data/local/tmp/metadata.db"
adb shell run-as deckers.thibault.aves.debug chmod 600 /data/user/0/deckers.thibault.aves.debug/databases/metadata.db

1
debug.sh Normal file
View file

@ -0,0 +1 @@
fvm flutter run -t lib/main_play.dart --flavor play 2>&1 | tee output.log

0
dynamicAlbums.csv Normal file
View file

7405
entry.csv Normal file

File diff suppressed because it is too large Load diff

0
favourites.csv Normal file
View file

View file

@ -1,3 +1,4 @@
// lib/model/db/db.dart
import 'package:aves/model/covers.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
@ -9,6 +10,9 @@ import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/viewer/video_playback.dart';
// 👇 serve per tipizzare il getter della connessione SQLite
import 'package:sqflite/sqflite.dart' show Database;
abstract class LocalMediaDb {
int get nextId;
@ -22,6 +26,9 @@ abstract class LocalMediaDb {
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes});
// === Espone la connessione SQLite aperta dal loader (per il sync 4A) ===
Database get rawDb;
// entries
Future<void> clearEntries();

View file

@ -25,6 +25,10 @@ import 'package:sqflite/sqflite.dart';
class SqfliteLocalMediaDb implements LocalMediaDb {
late Database _db;
// 🔴 4A: espone la connessione nativa aperta dal loader
@override
Database get rawDb => _db;
@override
Future<String> get path async => pContext.join(await getDatabasesPath(), 'metadata.db');
@ -50,24 +54,34 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> init() async {
_db = await openDatabase(
await path,
// --- IMPORTANT: configure DB for WAL and foreign keys to reduce locking ---
onConfigure: (db) async {
try {
await db.execute('PRAGMA journal_mode=WAL');
} catch (_) {}
try {
await db.execute('PRAGMA foreign_keys=ON');
} catch (_) {}
},
onCreate: (db, version) => SqfliteLocalMediaDbSchema.createLatestVersion(db),
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
version: 15,
);
// Log di servizio: path del DB usato dal loader
try {
final dbPath = await path;
debugPrint('[localMediaDb] opened db path=$dbPath');
} catch (_) {}
// Guardia idempotente: assicura colonne/indici utili anche per i remoti
await _ensureRemoteColumns(_db);
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
_lastId = (maxIdRows.firstOrNull?['maxId'] as int?) ?? 0;
// --- RIMOSSO: trigger di sync remoto in debug ---------------------------
// Il sync ora parte SOLO da HomePage (rrs.runRemoteSyncOnceManaged()) post-loading.
// if (kDebugMode) {
// // ignore: unawaited_futures
// remote.runRemoteSyncOnce(
// db: _db,
// baseUrl: 'https://prova.patachina.it',
// indexPath: 'photos',
// );
// }
}
@override
@ -90,7 +104,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
final batch = _db.batch();
const where = 'id = ?';
const coverWhere = 'entryId = ?';
@ -126,6 +139,7 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
@override
Future<Set<AvesEntry>> loadEntries({int? origin, String? directory}) async {
// Costruzione dinamica della WHERE
String? where;
final whereArgs = <Object?>[];
@ -134,7 +148,11 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
whereArgs.add(origin);
}
// 📌 Diagnostica: segna i parametri logici dingresso
debugPrint('[localMediaDb] loadEntries(origin=$origin, directory=$directory)');
final entries = <AvesEntry>{};
if (directory != null) {
final separator = pContext.separator;
if (!directory.endsWith(separator)) {
@ -143,22 +161,51 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
where = '${where != null ? '$where AND ' : ''}path LIKE ?';
whereArgs.add('$directory%');
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
debugPrint('[localMediaDb] SQL(queryCursor, by directory) WHERE="$where" args=$whereArgs');
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
final dirLength = directory.length;
var readRows = 0;
while (await cursor.moveNext()) {
final row = cursor.current;
readRows++;
// skip entries in subfolders
final path = row['path'] as String?;
if (path != null && !path.substring(dirLength).contains(separator)) {
entries.add(AvesEntry.fromMap(row));
}
}
debugPrint('[localMediaDb] rows read=$readRows kept=${entries.length} (directory filter)');
} else {
debugPrint('[localMediaDb] SQL(queryCursor) WHERE="${where ?? ''}" args=$whereArgs');
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
var readRows = 0;
while (await cursor.moveNext()) {
readRows++;
entries.add(AvesEntry.fromMap(cursor.current));
}
debugPrint('[localMediaDb] rows read=$readRows kept=${entries.length}');
// 🔎 Fallback di sola diagnostica: se stiamo cercando remoti (origin==1)
// ma non troviamo nulla, verifichiamo se in tabella ci sono righe origin=1
if ((origin == 1) && entries.isEmpty) {
try {
final countRows = await _db.rawQuery('SELECT COUNT(*) AS c FROM $entryTable WHERE origin=1');
final c = (countRows.firstOrNull?['c'] as int?) ?? 0;
debugPrint('[localMediaDb][diag] check COUNT(origin=1)=$c');
if (c > 0) {
final sample = await _db.rawQuery(
'SELECT id, remoteId, provider, uri, trashed, dateModifiedMillis '
'FROM $entryTable WHERE origin=1 ORDER BY id DESC LIMIT 5',
);
debugPrint('[localMediaDb][diag] sample origin=1 = $sample');
}
} catch (e, st) {
debugPrint('[localMediaDb][diag] fallback check failed: $e\n$st');
}
}
}
return entries;
@ -171,7 +218,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> insertEntries(Set<AvesEntry> entries) async {
if (entries.isEmpty) return;
final stopwatch = Stopwatch()..start();
// slice entries to avoid memory issues
int inserted = 0;
await Future.forEach(entries.slices(_entryInsertSliceMaxCount), (slice) async {
debugPrint('$runtimeType saveEntries inserting slice of [${inserted + 1}, ${inserted + slice.length}] entries');
@ -229,7 +275,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
if (duplicates.isNotEmpty) {
debugPrint('$runtimeType found duplicates=$duplicates');
}
// returns most recent duplicate for each duplicated content ID
return duplicates;
}
@ -409,7 +454,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> removeVaults(Set<VaultDetails> rows) async {
if (rows.isEmpty) return;
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
final batch = _db.batch();
rows.map((v) => v.name).forEach((name) => batch.delete(vaultTable, where: 'name = ?', whereArgs: [name]));
await batch.commit(noResult: true);
@ -496,11 +540,9 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> removeFavourites(Set<FavouriteRow> rows) async {
if (rows.isEmpty) return;
// estraiamo gli entryId dai FavouriteRow (evitando null)
final ids = rows.map((row) => row.entryId).whereType<int>().toSet();
if (ids.isEmpty) return;
// usando batch per efficienza
final batch = _db.batch();
for (final id in ids) {
batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id]);
@ -575,7 +617,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
}
});
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
final batch = _db.batch();
obsoleteFilterJson.forEach((filterJson) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filterJson]));
await batch.commit(noResult: true);
@ -607,8 +648,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
} catch (error, stack) {
debugPrint('$runtimeType failed to query table=$dynamicAlbumTable error=$error\n$stack');
if (bufferSize > 1) {
// a large row may prevent reading from the table because of cursor window size limit,
// so we retry without buffer to read as many rows as we can, and removing the others
debugPrint('$runtimeType retry to query table=$dynamicAlbumTable with no cursor buffer');
final safeRows = await loadAllDynamicAlbums(bufferSize: 1);
final clearedCount = await clearDynamicAlbums();
@ -643,7 +682,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> removeDynamicAlbums(Set<String> names) async {
if (names.isEmpty) return;
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
final batch = _db.batch();
names.forEach((name) => batch.delete(dynamicAlbumTable, where: 'name = ?', whereArgs: [name]));
await batch.commit(noResult: true);
@ -702,7 +740,6 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
Future<void> removeVideoPlayback(Set<int> ids) async {
if (ids.isEmpty) return;
// using array in `whereArgs` and using it with `where arg IN ?` is a pain, so we prefer `batch` instead
final batch = _db.batch();
ids.forEach((id) => batch.delete(videoPlaybackTable, where: 'id = ?', whereArgs: [id]));
await batch.commit(noResult: true);
@ -720,4 +757,44 @@ class SqfliteLocalMediaDb implements LocalMediaDb {
}
return result;
}
// --------------------------------------------------------
// MIGRAZIONE "GUARDIA" PER COLONNE REMOTE
// --------------------------------------------------------
Future<void> _ensureRemoteColumns(Database db) async {
Future<Set<String>> _columnsOf(String table) async {
final rows = await db.rawQuery('PRAGMA table_info($table)');
return rows.map((r) => (r['name'] as String).toLowerCase()).toSet();
}
final cols = await _columnsOf(entryTable);
final alters = <String>[
if (!cols.contains('origin')) "ALTER TABLE $entryTable ADD COLUMN origin INTEGER",
if (!cols.contains('trashed')) "ALTER TABLE $entryTable ADD COLUMN trashed INTEGER",
if (!cols.contains('provider')) "ALTER TABLE $entryTable ADD COLUMN provider TEXT",
if (!cols.contains('remoteid')) "ALTER TABLE $entryTable ADD COLUMN remoteId TEXT",
if (!cols.contains('remotepath')) "ALTER TABLE $entryTable ADD COLUMN remotePath TEXT",
if (!cols.contains('remotethumb1')) "ALTER TABLE $entryTable ADD COLUMN remoteThumb1 TEXT",
if (!cols.contains('remotethumb2')) "ALTER TABLE $entryTable ADD COLUMN remoteThumb2 TEXT",
if (!cols.contains('remotewidth')) "ALTER TABLE $entryTable ADD COLUMN remoteWidth INTEGER",
if (!cols.contains('remoteheight')) "ALTER TABLE $entryTable ADD COLUMN remoteHeight INTEGER",
if (!cols.contains('remoterotation')) "ALTER TABLE $entryTable ADD COLUMN remoteRotation INTEGER",
if (!cols.contains('latitude')) "ALTER TABLE $entryTable ADD COLUMN latitude REAL",
if (!cols.contains('longitude')) "ALTER TABLE $entryTable ADD COLUMN longitude REAL",
if (!cols.contains('altitude')) "ALTER TABLE $entryTable ADD COLUMN altitude REAL",
];
for (final sql in alters) {
try {
await db.execute(sql);
} catch (_) {
// idempotenza: ignora duplicate column name ecc.
}
}
// Indici utili ai remoti
try { await db.execute("CREATE INDEX IF NOT EXISTS entry_remote_idx ON $entryTable(origin, remoteId)"); } catch (_) {}
try { await db.execute("CREATE INDEX IF NOT EXISTS entry_origin_trashed_idx ON $entryTable(origin, trashed)"); } catch (_) {}
}
}

View file

@ -0,0 +1,791 @@
// lib/model/db/db_sqflite.dart
import 'dart:io';
import 'package:aves/model/covers.dart';
import 'package:aves/model/db/db.dart';
import 'package:aves/model/db/db_sqflite_schema.dart';
import 'package:aves/model/db/db_sqflite_upgrade.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/vaults/details.dart';
import 'package:aves/model/viewer/video_playback.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart';
// --- RIMOSSO: import del sync remoto dal layer DB ---
// import 'package:aves/remote/run_remote_sync.dart' as remote;
class SqfliteLocalMediaDb implements LocalMediaDb {
late Database _db;
// 🔴 4A: espone la connessione nativa aperta dal loader
@override
Database get rawDb => _db;
@override
Future<String> get path async => pContext.join(await getDatabasesPath(), 'metadata.db');
static const entryTable = SqfliteLocalMediaDbSchema.entryTable;
static const dateTakenTable = SqfliteLocalMediaDbSchema.dateTakenTable;
static const metadataTable = SqfliteLocalMediaDbSchema.metadataTable;
static const addressTable = SqfliteLocalMediaDbSchema.addressTable;
static const favouriteTable = SqfliteLocalMediaDbSchema.favouriteTable;
static const coverTable = SqfliteLocalMediaDbSchema.coverTable;
static const dynamicAlbumTable = SqfliteLocalMediaDbSchema.dynamicAlbumTable;
static const vaultTable = SqfliteLocalMediaDbSchema.vaultTable;
static const trashTable = SqfliteLocalMediaDbSchema.trashTable;
static const videoPlaybackTable = SqfliteLocalMediaDbSchema.videoPlaybackTable;
static const _entryInsertSliceMaxCount = 10000; // number of entries
static const _queryCursorBufferSize = 1000; // number of rows
static int _lastId = 0;
@override
int get nextId => ++_lastId;
@override
Future<void> init() async {
_db = await openDatabase(
await path,
onCreate: (db, version) => SqfliteLocalMediaDbSchema.createLatestVersion(db),
onUpgrade: LocalMediaDbUpgrader.upgradeDb,
version: 15,
);
// Log di servizio: path del DB usato dal loader
try {
final dbPath = await path;
debugPrint('[localMediaDb] opened db path=$dbPath');
} catch (_) {}
// ✅ Guardia idempotente: assicura colonne/indici utili anche per i remoti
await _ensureRemoteColumns(_db);
final maxIdRows = await _db.rawQuery('SELECT MAX(id) AS maxId FROM $entryTable');
_lastId = (maxIdRows.firstOrNull?['maxId'] as int?) ?? 0;
// --- RIMOSSO: trigger di sync remoto in debug ---------------------------
// Il sync ora parte SOLO da HomePage (rrs.runRemoteSyncOnceManaged()) post-loading.
}
@override
Future<int> dbFileSize() async {
final file = File(await path);
return await file.exists() ? await file.length() : 0;
}
@override
Future<void> reset() async {
debugPrint('$runtimeType reset');
await _db.close();
await deleteDatabase(await path);
await init();
}
@override
Future<void> removeIds(Set<int> ids, {Set<EntryDataType>? dataTypes}) async {
if (ids.isEmpty) return;
final _dataTypes = dataTypes ?? EntryDataType.values.toSet();
final batch = _db.batch();
const where = 'id = ?';
const coverWhere = 'entryId = ?';
ids.forEach((id) {
final whereArgs = [id];
if (_dataTypes.contains(EntryDataType.basic)) {
batch.delete(entryTable, where: where, whereArgs: whereArgs);
}
if (_dataTypes.contains(EntryDataType.catalog)) {
batch.delete(dateTakenTable, where: where, whereArgs: whereArgs);
batch.delete(metadataTable, where: where, whereArgs: whereArgs);
}
if (_dataTypes.contains(EntryDataType.address)) {
batch.delete(addressTable, where: where, whereArgs: whereArgs);
}
if (_dataTypes.contains(EntryDataType.references)) {
batch.delete(favouriteTable, where: where, whereArgs: whereArgs);
batch.delete(coverTable, where: coverWhere, whereArgs: whereArgs);
batch.delete(trashTable, where: where, whereArgs: whereArgs);
batch.delete(videoPlaybackTable, where: where, whereArgs: whereArgs);
}
});
await batch.commit(noResult: true);
}
// entries
@override
Future<void> clearEntries() async {
final count = await _db.delete(entryTable, where: '1');
debugPrint('$runtimeType clearEntries deleted $count rows');
}
@override
Future<Set<AvesEntry>> loadEntries({int? origin, String? directory}) async {
// Costruzione dinamica della WHERE
String? where;
final whereArgs = <Object?>[];
if (origin != null) {
where = 'origin = ?';
whereArgs.add(origin);
}
// 📌 Diagnostica: segna i parametri logici dingresso
debugPrint('[localMediaDb] loadEntries(origin=$origin, directory=$directory)');
final entries = <AvesEntry>{};
if (directory != null) {
final separator = pContext.separator;
if (!directory.endsWith(separator)) {
directory = '$directory$separator';
}
where = '${where != null ? '$where AND ' : ''}path LIKE ?';
whereArgs.add('$directory%');
debugPrint('[localMediaDb] SQL(queryCursor, by directory) WHERE="$where" args=$whereArgs');
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
final dirLength = directory.length;
var readRows = 0;
while (await cursor.moveNext()) {
final row = cursor.current;
readRows++;
// skip entries in subfolders
final path = row['path'] as String?;
if (path != null && !path.substring(dirLength).contains(separator)) {
entries.add(AvesEntry.fromMap(row));
}
}
debugPrint('[localMediaDb] rows read=$readRows kept=${entries.length} (directory filter)');
} else {
debugPrint('[localMediaDb] SQL(queryCursor) WHERE="${where ?? '—'}" args=$whereArgs');
final cursor = await _db.queryCursor(entryTable, where: where, whereArgs: whereArgs, bufferSize: _queryCursorBufferSize);
var readRows = 0;
while (await cursor.moveNext()) {
readRows++;
entries.add(AvesEntry.fromMap(cursor.current));
}
debugPrint('[localMediaDb] rows read=$readRows kept=${entries.length}');
// 🔎 Fallback di sola diagnostica: se stiamo cercando remoti (origin==1)
// ma non troviamo nulla, verifichiamo se in tabella ci sono righe origin=1
if ((origin == 1) && entries.isEmpty) {
try {
final countRows = await _db.rawQuery('SELECT COUNT(*) AS c FROM $entryTable WHERE origin=1');
final c = (countRows.firstOrNull?['c'] as int?) ?? 0;
debugPrint('[localMediaDb][diag] check COUNT(origin=1)=$c');
if (c > 0) {
final sample = await _db.rawQuery(
'SELECT id, remoteId, provider, uri, trashed, dateModifiedMillis '
'FROM $entryTable WHERE origin=1 ORDER BY id DESC LIMIT 5',
);
debugPrint('[localMediaDb][diag] sample origin=1 = $sample');
}
} catch (e, st) {
debugPrint('[localMediaDb][diag] fallback check failed: $e\n$st');
}
}
}
return entries;
}
@override
Future<Set<AvesEntry>> loadEntriesById(Set<int> ids) => _getByIds(ids, entryTable, AvesEntry.fromMap);
@override
Future<void> insertEntries(Set<AvesEntry> entries) async {
if (entries.isEmpty) return;
final stopwatch = Stopwatch()..start();
int inserted = 0;
await Future.forEach(entries.slices(_entryInsertSliceMaxCount), (slice) async {
debugPrint('$runtimeType saveEntries inserting slice of [${inserted + 1}, ${inserted + slice.length}] entries');
final batch = _db.batch();
slice.forEach((entry) => _batchInsertEntry(batch, entry));
await batch.commit(noResult: true);
inserted += slice.length;
});
debugPrint('$runtimeType saveEntries complete in ${stopwatch.elapsed.inMilliseconds}ms for ${entries.length} entries');
}
@override
Future<void> updateEntry(int id, AvesEntry entry) async {
final batch = _db.batch();
batch.delete(entryTable, where: 'id = ?', whereArgs: [id]);
_batchInsertEntry(batch, entry);
await batch.commit(noResult: true);
}
void _batchInsertEntry(Batch batch, AvesEntry entry) {
batch.insert(
entryTable,
entry.toDatabaseMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<Set<AvesEntry>> searchLiveEntries(String query, {int? limit}) async {
final rows = await _db.query(
entryTable,
where: '(title LIKE ? OR path LIKE ?) AND trashed = ?',
whereArgs: ['%$query%', '%$query%', 0],
orderBy: 'sourceDateTakenMillis DESC',
limit: limit,
);
return rows.map(AvesEntry.fromMap).toSet();
}
@override
Future<Set<AvesEntry>> searchLiveDuplicates(int origin, Set<AvesEntry>? entries) async {
String where = 'origin = ? AND trashed = ?';
if (entries != null) {
where += ' AND contentId IN (${entries.map((v) => v.contentId).join(',')})';
}
final rows = await _db.rawQuery(
'SELECT *, MAX(id) AS id'
' FROM $entryTable'
' WHERE $where'
' GROUP BY contentId'
' HAVING COUNT(id) > 1',
[origin, 0],
);
final duplicates = rows.map(AvesEntry.fromMap).toSet();
if (duplicates.isNotEmpty) {
debugPrint('$runtimeType found duplicates=$duplicates');
}
return duplicates;
}
// date taken
@override
Future<void> clearDates() async {
final count = await _db.delete(dateTakenTable, where: '1');
debugPrint('$runtimeType clearDates deleted $count rows');
}
@override
Future<Map<int?, int?>> loadDates() async {
final result = <int?, int?>{};
final cursor = await _db.queryCursor(dateTakenTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final row = cursor.current;
result[row['id'] as int] = row['dateMillis'] as int? ?? 0;
}
return result;
}
// catalog metadata
@override
Future<void> clearCatalogMetadata() async {
final count = await _db.delete(metadataTable, where: '1');
debugPrint('$runtimeType clearMetadataEntries deleted $count rows');
}
@override
Future<Set<CatalogMetadata>> loadCatalogMetadata() async {
final result = <CatalogMetadata>{};
final cursor = await _db.queryCursor(metadataTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(CatalogMetadata.fromMap(cursor.current));
}
return result;
}
@override
Future<Set<CatalogMetadata>> loadCatalogMetadataById(Set<int> ids) => _getByIds(ids, metadataTable, CatalogMetadata.fromMap);
@override
Future<void> saveCatalogMetadata(Set<CatalogMetadata> metadataEntries) async {
if (metadataEntries.isEmpty) return;
final stopwatch = Stopwatch()..start();
try {
final batch = _db.batch();
metadataEntries.forEach((metadata) => _batchInsertMetadata(batch, metadata));
await batch.commit(noResult: true);
debugPrint('$runtimeType saveMetadata complete in ${stopwatch.elapsed.inMilliseconds}ms for ${metadataEntries.length} entries');
} catch (error, stack) {
debugPrint('$runtimeType failed to save metadata with error=$error\n$stack');
}
}
@override
Future<void> updateCatalogMetadata(int id, CatalogMetadata? metadata) async {
final batch = _db.batch();
batch.delete(dateTakenTable, where: 'id = ?', whereArgs: [id]);
batch.delete(metadataTable, where: 'id = ?', whereArgs: [id]);
_batchInsertMetadata(batch, metadata);
await batch.commit(noResult: true);
}
void _batchInsertMetadata(Batch batch, CatalogMetadata? metadata) {
if (metadata == null) return;
if (metadata.dateMillis != 0) {
batch.insert(
dateTakenTable,
{
'id': metadata.id,
'dateMillis': metadata.dateMillis,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
batch.insert(
metadataTable,
metadata.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// address
@override
Future<void> clearAddresses() async {
final count = await _db.delete(addressTable, where: '1');
debugPrint('$runtimeType clearAddresses deleted $count rows');
}
@override
Future<Set<AddressDetails>> loadAddresses() async {
final result = <AddressDetails>{};
final cursor = await _db.queryCursor(addressTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(AddressDetails.fromMap(cursor.current));
}
return result;
}
@override
Future<Set<AddressDetails>> loadAddressesById(Set<int> ids) => _getByIds(ids, addressTable, AddressDetails.fromMap);
@override
Future<void> saveAddresses(Set<AddressDetails> addresses) async {
if (addresses.isEmpty) return;
final stopwatch = Stopwatch()..start();
final batch = _db.batch();
addresses.forEach((address) => _batchInsertAddress(batch, address));
await batch.commit(noResult: true);
debugPrint('$runtimeType saveAddresses complete in ${stopwatch.elapsed.inMilliseconds}ms for ${addresses.length} entries');
}
@override
Future<void> updateAddress(int id, AddressDetails? address) async {
final batch = _db.batch();
batch.delete(addressTable, where: 'id = ?', whereArgs: [id]);
_batchInsertAddress(batch, address);
await batch.commit(noResult: true);
}
void _batchInsertAddress(Batch batch, AddressDetails? address) {
if (address == null) return;
batch.insert(
addressTable,
address.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// vaults
@override
Future<void> clearVaults() async {
final count = await _db.delete(vaultTable, where: '1');
debugPrint('$runtimeType clearVaults deleted $count rows');
}
@override
Future<Set<VaultDetails>> loadAllVaults() async {
final result = <VaultDetails>{};
final cursor = await _db.queryCursor(vaultTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(VaultDetails.fromMap(cursor.current));
}
return result;
}
@override
Future<void> addVaults(Set<VaultDetails> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.forEach((row) => _batchInsertVault(batch, row));
await batch.commit(noResult: true);
}
@override
Future<void> updateVault(String oldName, VaultDetails row) async {
final batch = _db.batch();
batch.delete(vaultTable, where: 'name = ?', whereArgs: [oldName]);
_batchInsertVault(batch, row);
await batch.commit(noResult: true);
}
void _batchInsertVault(Batch batch, VaultDetails row) {
batch.insert(
vaultTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> removeVaults(Set<VaultDetails> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.map((v) => v.name).forEach((name) => batch.delete(vaultTable, where: 'name = ?', whereArgs: [name]));
await batch.commit(noResult: true);
}
// trash
@override
Future<void> clearTrashDetails() async {
final count = await _db.delete(trashTable, where: '1');
debugPrint('$runtimeType clearTrashDetails deleted $count rows');
}
@override
Future<Set<TrashDetails>> loadAllTrashDetails() async {
final result = <TrashDetails>{};
final cursor = await _db.queryCursor(trashTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(TrashDetails.fromMap(cursor.current));
}
return result;
}
@override
Future<void> updateTrash(int id, TrashDetails? details) async {
final batch = _db.batch();
batch.delete(trashTable, where: 'id = ?', whereArgs: [id]);
_batchInsertTrashDetails(batch, details);
await batch.commit(noResult: true);
}
void _batchInsertTrashDetails(Batch batch, TrashDetails? details) {
if (details == null) return;
batch.insert(
trashTable,
details.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// favourites
@override
Future<void> clearFavourites() async {
final count = await _db.delete(favouriteTable, where: '1');
debugPrint('$runtimeType clearFavourites deleted $count rows');
}
@override
Future<Set<FavouriteRow>> loadAllFavourites() async {
final result = <FavouriteRow>{};
final cursor = await _db.queryCursor(favouriteTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(FavouriteRow.fromMap(cursor.current));
}
return result;
}
@override
Future<void> addFavourites(Set<FavouriteRow> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.forEach((row) => _batchInsertFavourite(batch, row));
await batch.commit(noResult: true);
}
@override
Future<void> updateFavouriteId(int id, FavouriteRow row) async {
final batch = _db.batch();
batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id]);
_batchInsertFavourite(batch, row);
await batch.commit(noResult: true);
}
void _batchInsertFavourite(Batch batch, FavouriteRow row) {
batch.insert(
favouriteTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> removeFavourites(Set<FavouriteRow> rows) async {
if (rows.isEmpty) return;
final ids = rows.map((row) => row.entryId).whereType<int>().toSet();
if (ids.isEmpty) return;
final batch = _db.batch();
for (final id in ids) {
batch.delete(favouriteTable, where: 'id = ?', whereArgs: [id]);
}
await batch.commit(noResult: true);
}
// covers
@override
Future<void> clearCovers() async {
final count = await _db.delete(coverTable, where: '1');
debugPrint('$runtimeType clearCovers deleted $count rows');
}
@override
Future<Set<CoverRow>> loadAllCovers() async {
final result = <CoverRow>{};
final cursor = await _db.queryCursor(coverTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final rowMap = cursor.current;
final row = CoverRow.fromMap(rowMap);
if (row != null) {
result.add(row);
} else {
debugPrint('$runtimeType failed to deserialize cover from row=$rowMap');
}
}
return result;
}
@override
Future<void> addCovers(Set<CoverRow> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.forEach((row) => _batchInsertCover(batch, row));
await batch.commit(noResult: true);
}
@override
Future<void> updateCoverEntryId(int id, CoverRow row) async {
final batch = _db.batch();
batch.delete(coverTable, where: 'entryId = ?', whereArgs: [id]);
_batchInsertCover(batch, row);
await batch.commit(noResult: true);
}
void _batchInsertCover(Batch batch, CoverRow row) {
batch.insert(
coverTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> removeCovers(Set<CollectionFilter> filters) async {
if (filters.isEmpty) return;
// for backward compatibility, remove stored JSON instead of removing de/reserialized filters
final obsoleteFilterJson = <String>{};
final rows = await _db.query(coverTable);
rows.forEach((row) {
final filterJson = row['filter'] as String?;
if (filterJson != null) {
final filter = CollectionFilter.fromJson(filterJson);
if (filters.any((v) => filter == v)) {
obsoleteFilterJson.add(filterJson);
}
}
});
final batch = _db.batch();
obsoleteFilterJson.forEach((filterJson) => batch.delete(coverTable, where: 'filter = ?', whereArgs: [filterJson]));
await batch.commit(noResult: true);
}
// dynamic albums
@override
Future<int> clearDynamicAlbums() async {
final count = await _db.delete(dynamicAlbumTable, where: '1');
debugPrint('$runtimeType clearDynamicAlbums deleted $count rows');
return count;
}
@override
Future<Set<DynamicAlbumRow>> loadAllDynamicAlbums({int bufferSize = _queryCursorBufferSize}) async {
final result = <DynamicAlbumRow>{};
try {
final cursor = await _db.queryCursor(dynamicAlbumTable, bufferSize: bufferSize);
while (await cursor.moveNext()) {
final rowMap = cursor.current;
final row = DynamicAlbumRow.fromMap(rowMap);
if (row != null) {
result.add(row);
} else {
debugPrint('$runtimeType failed to deserialize dynamic album from row=$rowMap');
}
}
} catch (error, stack) {
debugPrint('$runtimeType failed to query table=$dynamicAlbumTable error=$error\n$stack');
if (bufferSize > 1) {
debugPrint('$runtimeType retry to query table=$dynamicAlbumTable with no cursor buffer');
final safeRows = await loadAllDynamicAlbums(bufferSize: 1);
final clearedCount = await clearDynamicAlbums();
await addDynamicAlbums(safeRows);
final addedCount = safeRows.length;
final lostCount = clearedCount - addedCount;
debugPrint('$runtimeType kept $addedCount rows, lost $lostCount rows from table=$dynamicAlbumTable');
return safeRows;
}
}
return result;
}
@override
Future<void> addDynamicAlbums(Set<DynamicAlbumRow> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.forEach((row) => _batchInsertDynamicAlbum(batch, row));
await batch.commit(noResult: true);
}
void _batchInsertDynamicAlbum(Batch batch, DynamicAlbumRow row) {
batch.insert(
dynamicAlbumTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> removeDynamicAlbums(Set<String> names) async {
if (names.isEmpty) return;
final batch = _db.batch();
names.forEach((name) => batch.delete(dynamicAlbumTable, where: 'name = ?', whereArgs: [name]));
await batch.commit(noResult: true);
}
// video playback
@override
Future<void> clearVideoPlayback() async {
final count = await _db.delete(videoPlaybackTable, where: '1');
debugPrint('$runtimeType clearVideoPlayback deleted $count rows');
}
@override
Future<Set<VideoPlaybackRow>> loadAllVideoPlayback() async {
final result = <VideoPlaybackRow>{};
final cursor = await _db.queryCursor(videoPlaybackTable, bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
final rowMap = cursor.current;
final row = VideoPlaybackRow.fromMap(rowMap);
if (row != null) {
result.add(row);
} else {
debugPrint('$runtimeType failed to deserialize video playback from row=$rowMap');
}
}
return result;
}
@override
Future<VideoPlaybackRow?> loadVideoPlayback(int id) async {
final rows = await _db.query(videoPlaybackTable, where: 'id = ?', whereArgs: [id]);
if (rows.isEmpty) return null;
return VideoPlaybackRow.fromMap(rows.first);
}
@override
Future<void> addVideoPlayback(Set<VideoPlaybackRow> rows) async {
if (rows.isEmpty) return;
final batch = _db.batch();
rows.forEach((row) => _batchInsertVideoPlayback(batch, row));
await batch.commit(noResult: true);
}
void _batchInsertVideoPlayback(Batch batch, VideoPlaybackRow row) {
batch.insert(
videoPlaybackTable,
row.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<void> removeVideoPlayback(Set<int> ids) async {
if (ids.isEmpty) return;
final batch = _db.batch();
ids.forEach((id) => batch.delete(videoPlaybackTable, where: 'id = ?', whereArgs: [id]));
await batch.commit(noResult: true);
}
// convenience methods
Future<Set<T>> _getByIds<T>(Set<int> ids, String table, T Function(Map<String, Object?> row) mapRow) async {
final result = <T>{};
if (ids.isNotEmpty) {
final cursor = await _db.queryCursor(table, where: 'id IN (${ids.join(',')})', bufferSize: _queryCursorBufferSize);
while (await cursor.moveNext()) {
result.add(mapRow(cursor.current));
}
}
return result;
}
// --------------------------------------------------------
// MIGRAZIONE "GUARDIA" PER COLONNE REMOTE
// --------------------------------------------------------
Future<void> _ensureRemoteColumns(Database db) async {
Future<Set<String>> _columnsOf(String table) async {
final rows = await db.rawQuery('PRAGMA table_info($table)');
return rows.map((r) => (r['name'] as String).toLowerCase()).toSet();
}
final cols = await _columnsOf(entryTable);
final alters = <String>[
if (!cols.contains('origin')) "ALTER TABLE $entryTable ADD COLUMN origin INTEGER",
if (!cols.contains('trashed')) "ALTER TABLE $entryTable ADD COLUMN trashed INTEGER",
if (!cols.contains('provider')) "ALTER TABLE $entryTable ADD COLUMN provider TEXT",
if (!cols.contains('remoteid')) "ALTER TABLE $entryTable ADD COLUMN remoteId TEXT",
if (!cols.contains('remotepath')) "ALTER TABLE $entryTable ADD COLUMN remotePath TEXT",
if (!cols.contains('remotethumb1')) "ALTER TABLE $entryTable ADD COLUMN remoteThumb1 TEXT",
if (!cols.contains('remotethumb2')) "ALTER TABLE $entryTable ADD COLUMN remoteThumb2 TEXT",
if (!cols.contains('remotewidth')) "ALTER TABLE $entryTable ADD COLUMN remoteWidth INTEGER",
if (!cols.contains('remoteheight')) "ALTER TABLE $entryTable ADD COLUMN remoteHeight INTEGER",
if (!cols.contains('remoterotation')) "ALTER TABLE $entryTable ADD COLUMN remoteRotation INTEGER",
if (!cols.contains('latitude')) "ALTER TABLE $entryTable ADD COLUMN latitude REAL",
if (!cols.contains('longitude')) "ALTER TABLE $entryTable ADD COLUMN longitude REAL",
if (!cols.contains('altitude')) "ALTER TABLE $entryTable ADD COLUMN altitude REAL",
];
for (final sql in alters) {
try {
await db.execute(sql);
} catch (_) {
// idempotenza: ignora duplicate column name ecc.
}
}
// Indici utili ai remoti
try { await db.execute("CREATE INDEX IF NOT EXISTS entry_remote_idx ON $entryTable(origin, remoteId)"); } catch (_) {}
try { await db.execute("CREATE INDEX IF NOT EXISTS entry_origin_trashed_idx ON $entryTable(origin, trashed)"); } catch (_) {}
}
}

View file

@ -59,7 +59,7 @@ class AvesEntry with AvesEntryBase {
addressChangeNotifier = AChangeNotifier();
// ============================================================
// CAMPI REMOTI (AGGIUNTI DA TE)
// CAMPI REMOTI (AGGIUNTI)
// ============================================================
String? remoteId;
@ -74,10 +74,13 @@ class AvesEntry with AvesEntryBase {
int? remoteWidth;
int? remoteHeight;
int? remoteRotation;
// Toggle: se true il decoder remoto rispetta già lEXIF Aves NON ruota.
static const bool kRemoteRespectsExifAtDecode = true;
// Getter utili
bool get isRemote => origin == 1;
bool get isRemote => origin == 1; // EntryOrigins.remote == 1
String? get remoteThumb => remoteThumb2 ?? remoteThumb1;
// ============================================================
@ -113,6 +116,7 @@ class AvesEntry with AvesEntryBase {
this.altitude,
this.remoteWidth,
this.remoteHeight,
this.remoteRotation,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated(
@ -172,6 +176,9 @@ class AvesEntry with AvesEntryBase {
latitude: latitude,
longitude: longitude,
altitude: altitude,
remoteWidth: remoteWidth,
remoteHeight: remoteHeight,
remoteRotation: remoteRotation,
)
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
..addressDetails = _addressDetails?.copyWith(id: copyEntryId)
@ -181,37 +188,66 @@ class AvesEntry with AvesEntryBase {
}
// ============================================================
// FROM MAP (DB MODEL)
// FROM MAP (DB MODEL) REMOTE-FRIENDLY
// ============================================================
factory AvesEntry.fromMap(Map map) {
// origin/remoteId/uri fallback corretti
final origin = map[EntryFields.origin] as int? ?? 0;
final rid = map['remoteId'] as String?;
final rawUri = map[EntryFields.uri] as String?;
final safeUri = rawUri ??
((origin == 1 && rid != null) ? 'aves-remote://rid/$rid' : 'content://invalid');
// MIME robusto (source -> generale -> inferenza -> default)
final safeMime = (map[EntryFields.sourceMimeType] as String?) ??
(map[EntryFields.mimeType] as String?) ??
_inferMimeFromRemotePath(map) ??
'image/jpeg';
// Dimensioni: usa remoteWidth/remoteHeight se mancano le locali
final safeWidth =
(map[EntryFields.width] as int?) ?? (map['remoteWidth'] as int?) ?? 0;
final safeHeight =
(map[EntryFields.height] as int?) ?? (map['remoteHeight'] as int?) ?? 0;
// dateModified: scatto -> ora
final safeDateModified = (map[EntryFields.dateModifiedMillis] as int?) ??
(map[EntryFields.sourceDateTakenMillis] as int?) ??
DateTime.now().millisecondsSinceEpoch;
// contentId sintetico se NULL
final safeContentId = (map[EntryFields.contentId] as int?) ??
_syntheticContentId(map);
return AvesEntry(
id: map[EntryFields.id] as int?,
uri: (map[EntryFields.uri] as String?) ?? 'remote://missing',
uri: safeUri,
path: map[EntryFields.path] as String?,
pageId: null,
contentId: map[EntryFields.contentId] as int?,
sourceMimeType: map[EntryFields.sourceMimeType] as String,
width: map[EntryFields.width] as int? ?? 0,
height: map[EntryFields.height] as int? ?? 0,
contentId: safeContentId,
sourceMimeType: safeMime,
width: safeWidth,
height: safeHeight,
sourceRotationDegrees: map[EntryFields.sourceRotationDegrees] as int? ?? 0,
sizeBytes: map[EntryFields.sizeBytes] as int?,
sourceTitle: map[EntryFields.title] as String?,
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
dateModifiedMillis: map[EntryFields.dateModifiedMillis] as int?,
dateModifiedMillis: safeDateModified,
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
durationMillis: map[EntryFields.durationMillis] as int?,
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
origin: map[EntryFields.origin] as int,
origin: origin,
// --- REMOTE FIELDS ---
remoteId: map['remoteId'] as String?,
remoteId: rid,
remotePath: map['remotePath'] as String?,
remoteThumb1: map['remoteThumb1'] as String?,
remoteThumb2: map['remoteThumb2'] as String?,
provider: map['provider'] as String?,
remoteWidth: map['remoteWidth'] as int?,
remoteHeight: map['remoteHeight'] as int?,
remoteRotation: map['remoteRotation'] as int?,
latitude: (map['latitude'] as num?)?.toDouble(),
longitude: (map['longitude'] as num?)?.toDouble(),
@ -250,6 +286,7 @@ class AvesEntry with AvesEntryBase {
'provider': provider,
'remoteWidth': remoteWidth,
'remoteHeight': remoteHeight,
'remoteRotation': remoteRotation,
'latitude': latitude,
'longitude': longitude,
@ -258,11 +295,126 @@ class AvesEntry with AvesEntryBase {
}
// ============================================================
// (TUTTO IL RESTO È IDENTICO ALLA VERSIONE ORIGINALE AVES)
// GETTER REMOTE-AWARE (display/size/rotation + visibilità)
// ============================================================
// ... (qui rimane invariato tutto il codice originale che avevi già)
@override
int get rotationDegrees {
if (isRemote) {
// Decoder remoto rispetta già l'EXIF → Aves non ruota
if (kRemoteRespectsExifAtDecode) return 0;
// Altrimenti, usa la rotazione remota (se disponibile)
return remoteRotation ?? 0;
}
return _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
}
@override
set rotationDegrees(int rotationDegrees) {
if (isRemote) {
// Manteniamo il valore per future policy, ma non verrà applicato se kRemoteRespectsExifAtDecode=true
remoteRotation = rotationDegrees;
} else {
sourceRotationDegrees = rotationDegrees;
_catalogMetadata?.rotationDegrees = rotationDegrees;
}
}
// 🔁 isFlipped (per i remoti: sempre false)
@override
bool get isFlipped => isRemote ? false : (_catalogMetadata?.isFlipped ?? false);
@override
set isFlipped(bool v) {
if (!isRemote) {
_catalogMetadata?.isFlipped = v;
}
}
// =======================
// PATCH sui DUE GETTER
// =======================
@override
double get displayAspectRatio {
if (isRemote) {
// dimensioni "naturali" dei remoti
double w = (remoteWidth ?? width).toDouble();
double h = (remoteHeight ?? height).toDouble();
// Se il decoder remoto rispetta lEXIF e loriginale era 90/270,
// il bitmap decodificato è già portrait: per il FRAME swappiamo w/h.
if (kRemoteRespectsExifAtDecode) {
final rotated90 = ((remoteRotation ?? 0) % 180) == 90;
if (rotated90) {
final t = w; w = h; h = t;
}
}
if (w == 0 || h == 0) return 1;
return w / h;
}
// Locali: logica originale
double w = width.toDouble();
double h = height.toDouble();
if (w == 0 || h == 0) return 1;
return isRotated ? h / w : w / h;
}
@override
Size get displaySize {
if (isRemote) {
double w = (remoteWidth ?? width).toDouble();
double h = (remoteHeight ?? height).toDouble();
if (kRemoteRespectsExifAtDecode) {
final rotated90 = ((remoteRotation ?? 0) % 180) == 90;
if (rotated90) {
final t = w; w = h; h = t;
}
}
if (w == 0 || h == 0) return const Size(1, 1);
// Nessuno swap aggiuntivo lato Aves: il frame ora è coerente al bitmap
return Size(w, h);
}
// Locali: logica originale
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
/// Presenza logica: i remoti non vivono sul FS locale considera presenti se non cestinati.
bool get isPresent {
if (isRemote) return !trashed;
return !trashed &&
(uri.startsWith('content://') ||
uri.startsWith('file://') ||
path != null);
}
/// Visualizzabilità minima: MIME coerente + dimensioni non zero.
bool get isDisplayable {
if (trashed) return false;
if (isRemote) {
final m = mimeType;
final supported = m.startsWith('image/') || m.startsWith('video/');
final hasSize =
(width > 0 && height > 0) || (remoteWidth != null && remoteHeight != null);
return supported && hasSize;
}
final m = mimeType;
return (m.startsWith('image/') || m.startsWith('video/')) && isPresent;
}
/// Le thumbs remote non dipendono dal canale nativo basta isDisplayable.
bool get canThumbnail => isDisplayable;
// ============================================================
// (RESTO INVARIATO)
// ============================================================
Map<String, dynamic> toPlatformEntryMap() {
return {
@ -291,9 +443,6 @@ class AvesEntry with AvesEntryBase {
addressChangeNotifier.dispose();
}
// do not implement [Object.==] and [Object.hashCode] using mutable attributes (e.g. `uri`)
// so that we can reliably use instances in a `Set`, which requires consistent hash codes over time
@override
String toString() => '$runtimeType#${shortHash(this)}{id=$id, uri=$uri, path=$path, pageId=$pageId}';
@ -335,7 +484,9 @@ class AvesEntry with AvesEntryBase {
DateTime? _bestDate;
DateTime? get bestDate {
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis(dateModifiedMillis ?? 0);
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ??
dateTimeFromMillis(sourceDateTakenMillis) ??
dateTimeFromMillis(dateModifiedMillis ?? 0);
return _bestDate;
}
@ -346,50 +497,8 @@ class AvesEntry with AvesEntryBase {
int get rating => _catalogMetadata?.rating ?? 0;
@override
int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
set rotationDegrees(int rotationDegrees) {
sourceRotationDegrees = rotationDegrees;
_catalogMetadata?.rotationDegrees = rotationDegrees;
}
bool get isFlipped => _catalogMetadata?.isFlipped ?? false;
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
// so it should be registered as width=1920, height=1080, orientation=90,
// but is incorrectly registered as width=1080, height=1920, orientation=0.
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
// Comparing width and height can help with the portrait FHD video example,
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
bool get isRotated => rotationDegrees % 180 == 90;
@override
double get displayAspectRatio {
if (width == 0 || height == 0) return 1;
return isRotated ? height / width : width / height;
}
@override
Size get displaySize {
// PATCH: dimensioni corrette per immagini remote
if (isRemote && remoteWidth != null && remoteHeight != null) {
final w = remoteWidth!.toDouble();
final h = remoteHeight!.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
// fallback originale Aves
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
String? get sourceTitle => _sourceTitle;
set sourceTitle(String? sourceTitle) {
@ -404,13 +513,11 @@ Size get displaySize {
_bestDate = null;
}
// TODO TLAD cache _monthTaken
DateTime? get monthTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month);
}
// TODO TLAD cache _dayTaken
DateTime? get dayTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month, d.day);
@ -431,15 +538,12 @@ Size get displaySize {
return _durationText!;
}
// returns whether this entry has GPS coordinates
// (0, 0) coordinates are considered invalid, as it is likely a default value
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
bool get hasAddress => _addressDetails != null;
// has a place, or at least the full country name
// derived from Google reverse geocoding addresses
bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
bool get hasFineAddress =>
_addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
Set<String>? _tags;
@ -452,7 +556,9 @@ Size get displaySize {
@override
String? get bestTitle {
_bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true ? _catalogMetadata!.xmpTitle : (filenameWithoutExtension ?? sourceTitle);
_bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true
? _catalogMetadata!.xmpTitle
: (filenameWithoutExtension ?? sourceTitle);
return _bestTitle;
}
@ -493,8 +599,6 @@ Size get displaySize {
}
String get shortAddress {
// `admin area` examples: Seoul, Geneva, null
// `locality` examples: Mapo-gu, Geneva, Annecy
return {
_addressDetails?.countryName,
_addressDetails?.adminArea,
@ -563,7 +667,6 @@ Size get displaySize {
required bool persist,
required Set<EntryDataType> dataTypes,
}) async {
// clear derived fields
_bestDate = null;
_bestTitle = null;
_tags = null;
@ -594,16 +697,42 @@ Size get displaySize {
return opCompleter.future;
}
// when the MIME type or the image itself changed (e.g. after rotation)
Future<void> _onVisualFieldChanged(
String oldMimeType,
int? oldDateModifiedMillis,
int oldRotationDegrees,
bool oldIsFlipped,
) async {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedMillis != dateModifiedMillis || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) ||
oldDateModifiedMillis != dateModifiedMillis ||
oldRotationDegrees != rotationDegrees ||
oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped, isAnimated);
visualChangeNotifier.notify();
}
}
// ------------------------------------------------------------
// Helpers remote-friendly
// ------------------------------------------------------------
static String? _inferMimeFromRemotePath(Map map) {
final path = (map['remotePath'] as String?) ?? (map[EntryFields.path] as String?);
if (path == null) return null;
final lower = path.toLowerCase();
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
if (lower.endsWith('.png')) return 'image/png';
if (lower.endsWith('.webp')) return 'image/webp';
if (lower.endsWith('.gif')) return 'image/gif';
if (lower.endsWith('.mp4')) return 'video/mp4';
if (lower.endsWith('.mov')) return 'video/quicktime';
if (lower.endsWith('.mkv')) return 'video/x-matroska';
return null;
}
static int? _syntheticContentId(Map map) {
final id = map[EntryFields.id] as int?;
if (id == null) return null;
return 1000000000 + id; // disgiunto dai locali, >0
}
}

View file

@ -0,0 +1,718 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
enum EntryDataType { basic, aspectRatio, catalog, address, references }
class AvesEntry with AvesEntryBase {
// ============================================================
// CAMPI ORIGINALI AVES
// ============================================================
@override
int id;
@override
String uri;
@override
int? pageId;
@override
int? sizeBytes;
String? _path, _filename, _extension, _sourceTitle;
EntryDir? _directory;
int? contentId;
final String sourceMimeType;
int width, height, sourceRotationDegrees;
int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
bool trashed;
int origin;
int? _catalogDateMillis;
CatalogMetadata? _catalogMetadata;
AddressDetails? _addressDetails;
TrashDetails? trashDetails;
List<AvesEntry>? stackedEntries;
@override
final AChangeNotifier visualChangeNotifier = AChangeNotifier();
final AChangeNotifier metadataChangeNotifier = AChangeNotifier(),
addressChangeNotifier = AChangeNotifier();
// ============================================================
// CAMPI REMOTI (AGGIUNTI)
// ============================================================
String? remoteId;
String? remotePath;
String? remoteThumb1;
String? remoteThumb2;
String? provider;
double? latitude;
double? longitude;
double? altitude;
int? remoteWidth;
int? remoteHeight;
int? remoteRotation;
// Toggle: se true il decoder remoto rispetta già lEXIF → Aves NON ruota.
static const bool kRemoteRespectsExifAtDecode = true;
// Getter utili
bool get isRemote => origin == 1; // EntryOrigins.remote == 1
String? get remoteThumb => remoteThumb2 ?? remoteThumb1;
// ============================================================
// COSTRUTTORE
// ============================================================
AvesEntry({
required int? id,
required this.uri,
required String? path,
required this.contentId,
required this.pageId,
required this.sourceMimeType,
required this.width,
required this.height,
required this.sourceRotationDegrees,
required this.sizeBytes,
required String? sourceTitle,
required this.dateAddedSecs,
required int? dateModifiedMillis,
required this.sourceDateTakenMillis,
required int? durationMillis,
required this.trashed,
required this.origin,
this.stackedEntries,
this.remoteId,
this.remotePath,
this.remoteThumb1,
this.remoteThumb2,
this.provider,
this.latitude,
this.longitude,
this.altitude,
this.remoteWidth,
this.remoteHeight,
this.remoteRotation,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated(
library: 'aves',
className: '$AvesEntry',
object: this,
);
}
this.path = path;
this.sourceTitle = sourceTitle;
this.dateModifiedMillis = dateModifiedMillis;
this.durationMillis = durationMillis;
}
// ============================================================
// COPY-WITH
// ============================================================
AvesEntry copyWith({
int? id,
String? uri,
String? path,
int? contentId,
String? title,
int? dateAddedSecs,
int? dateModifiedMillis,
int? origin,
List<AvesEntry>? stackedEntries,
}) {
final copyEntryId = id ?? this.id;
final copied = AvesEntry(
id: copyEntryId,
uri: uri ?? this.uri,
path: path ?? this.path,
contentId: contentId ?? this.contentId,
pageId: null,
sourceMimeType: sourceMimeType,
width: width,
height: height,
sourceRotationDegrees: sourceRotationDegrees,
sizeBytes: sizeBytes,
sourceTitle: title ?? sourceTitle,
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis,
trashed: trashed,
origin: origin ?? this.origin,
stackedEntries: stackedEntries ?? this.stackedEntries,
// campi remoti copiati
remoteId: remoteId,
remotePath: remotePath,
remoteThumb1: remoteThumb1,
remoteThumb2: remoteThumb2,
provider: provider,
latitude: latitude,
longitude: longitude,
altitude: altitude,
remoteWidth: remoteWidth,
remoteHeight: remoteHeight,
remoteRotation: remoteRotation,
)
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
..addressDetails = _addressDetails?.copyWith(id: copyEntryId)
..trashDetails = trashDetails?.copyWith(id: copyEntryId);
return copied;
}
// ============================================================
// FROM MAP (DB → MODEL) — REMOTE-FRIENDLY
// ============================================================
factory AvesEntry.fromMap(Map map) {
// origin/remoteId/uri → fallback corretti
final origin = map[EntryFields.origin] as int? ?? 0;
final rid = map['remoteId'] as String?;
final rawUri = map[EntryFields.uri] as String?;
final safeUri = rawUri ??
((origin == 1 && rid != null) ? 'aves-remote://rid/$rid' : 'content://invalid');
// MIME robusto (source -> generale -> inferenza -> default)
final safeMime = (map[EntryFields.sourceMimeType] as String?) ??
(map[EntryFields.mimeType] as String?) ??
_inferMimeFromRemotePath(map) ??
'image/jpeg';
// Dimensioni: usa remoteWidth/remoteHeight se mancano le locali
final safeWidth =
(map[EntryFields.width] as int?) ?? (map['remoteWidth'] as int?) ?? 0;
final safeHeight =
(map[EntryFields.height] as int?) ?? (map['remoteHeight'] as int?) ?? 0;
// dateModified: scatto -> ora
final safeDateModified = (map[EntryFields.dateModifiedMillis] as int?) ??
(map[EntryFields.sourceDateTakenMillis] as int?) ??
DateTime.now().millisecondsSinceEpoch;
// contentId sintetico se NULL
final safeContentId = (map[EntryFields.contentId] as int?) ??
_syntheticContentId(map);
return AvesEntry(
id: map[EntryFields.id] as int?,
uri: safeUri,
path: map[EntryFields.path] as String?,
pageId: null,
contentId: safeContentId,
sourceMimeType: safeMime,
width: safeWidth,
height: safeHeight,
sourceRotationDegrees: map[EntryFields.sourceRotationDegrees] as int? ?? 0,
sizeBytes: map[EntryFields.sizeBytes] as int?,
sourceTitle: map[EntryFields.title] as String?,
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
dateModifiedMillis: safeDateModified,
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
durationMillis: map[EntryFields.durationMillis] as int?,
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
origin: origin,
// --- REMOTE FIELDS ---
remoteId: rid,
remotePath: map['remotePath'] as String?,
remoteThumb1: map['remoteThumb1'] as String?,
remoteThumb2: map['remoteThumb2'] as String?,
provider: map['provider'] as String?,
remoteWidth: map['remoteWidth'] as int?,
remoteHeight: map['remoteHeight'] as int?,
remoteRotation: map['remoteRotation'] as int?,
latitude: (map['latitude'] as num?)?.toDouble(),
longitude: (map['longitude'] as num?)?.toDouble(),
altitude: (map['altitude'] as num?)?.toDouble(),
);
}
// ============================================================
// TO MAP (MODEL → DB)
// ============================================================
Map<String, dynamic> toDatabaseMap() {
return {
EntryFields.id: id,
EntryFields.uri: uri,
EntryFields.path: path,
EntryFields.contentId: contentId,
EntryFields.sourceMimeType: sourceMimeType,
EntryFields.width: width,
EntryFields.height: height,
EntryFields.sourceRotationDegrees: sourceRotationDegrees,
EntryFields.sizeBytes: sizeBytes,
EntryFields.title: sourceTitle,
EntryFields.dateAddedSecs: dateAddedSecs,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
EntryFields.durationMillis: durationMillis,
EntryFields.trashed: trashed ? 1 : 0,
EntryFields.origin: origin,
// --- REMOTE FIELDS ---
'remoteId': remoteId,
'remotePath': remotePath,
'remoteThumb1': remoteThumb1,
'remoteThumb2': remoteThumb2,
'provider': provider,
'remoteWidth': remoteWidth,
'remoteHeight': remoteHeight,
'remoteRotation': remoteRotation,
'latitude': latitude,
'longitude': longitude,
'altitude': altitude,
};
}
// ============================================================
// GETTER “REMOTE-AWARE” (display/size/rotation + visibilità)
// ============================================================
@override
int get rotationDegrees {
if (isRemote) {
// Decoder remoto rispetta già l'EXIF → Aves non ruota
if (kRemoteRespectsExifAtDecode) return 0;
// Altrimenti, usa la rotazione remota (se disponibile)
return remoteRotation ?? 0;
}
return _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
}
@override
set rotationDegrees(int rotationDegrees) {
if (isRemote) {
// Manteniamo il valore per future policy, ma non verrà applicato se kRemoteRespectsExifAtDecode=true
remoteRotation = rotationDegrees;
} else {
sourceRotationDegrees = rotationDegrees;
_catalogMetadata?.rotationDegrees = rotationDegrees;
}
}
// 🔁 isFlipped (per i remoti: sempre false)
@override
bool get isFlipped => isRemote ? false : (_catalogMetadata?.isFlipped ?? false);
@override
set isFlipped(bool v) {
if (!isRemote) {
_catalogMetadata?.isFlipped = v;
}
}
@override
double get displayAspectRatio {
if (isRemote) {
// Usa le dimensioni remote così come sono, senza swap basato su rotazione
final w = (remoteWidth ?? width).toDouble();
final h = (remoteHeight ?? height).toDouble();
if (w == 0 || h == 0) return 1;
return w / h;
}
// Locali: logica originale
double w = width.toDouble();
double h = height.toDouble();
if (w == 0 || h == 0) return 1;
return isRotated ? h / w : w / h;
}
@override
Size get displaySize {
if (isRemote) {
final w = (remoteWidth ?? width).toDouble();
final h = (remoteHeight ?? height).toDouble();
if (w == 0 || h == 0) return const Size(1, 1);
// NIENTE swap per rotazione lato Aves
return Size(w, h);
}
// Locali: logica originale
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
// --- VISIBILITÀ: non richiedere filesystem per i remoti ---
/// Presenza “logica”: i remoti non vivono sul FS locale → considera presenti se non cestinati.
bool get isPresent {
if (isRemote) return !trashed;
return !trashed &&
(uri.startsWith('content://') ||
uri.startsWith('file://') ||
path != null);
}
/// Visualizzabilità minima: MIME coerente + dimensioni non zero.
bool get isDisplayable {
if (trashed) return false;
if (isRemote) {
final m = mimeType;
final supported = m.startsWith('image/') || m.startsWith('video/');
final hasSize =
(width > 0 && height > 0) || (remoteWidth != null && remoteHeight != null);
return supported && hasSize;
}
final m = mimeType;
return (m.startsWith('image/') || m.startsWith('video/')) && isPresent;
}
/// Le thumbs remote non dipendono dal canale nativo → basta isDisplayable.
bool get canThumbnail => isDisplayable;
// ============================================================
// (RESTO INVARIATO)
// ============================================================
Map<String, dynamic> toPlatformEntryMap() {
return {
EntryFields.uri: uri,
EntryFields.path: path,
EntryFields.pageId: pageId,
EntryFields.mimeType: mimeType,
EntryFields.width: width,
EntryFields.height: height,
EntryFields.rotationDegrees: rotationDegrees,
EntryFields.isFlipped: isFlipped,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sizeBytes: sizeBytes,
EntryFields.trashed: trashed,
EntryFields.trashPath: trashDetails?.path,
EntryFields.origin: origin,
};
}
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectDisposed(object: this);
}
visualChangeNotifier.dispose();
metadataChangeNotifier.dispose();
addressChangeNotifier.dispose();
}
@override
String toString() => '$runtimeType#${shortHash(this)}{id=$id, uri=$uri, path=$path, pageId=$pageId}';
set path(String? path) {
_path = path;
_directory = null;
_filename = null;
_extension = null;
_bestTitle = null;
}
@override
String? get path => _path;
// directory path, without the trailing separator
String? get directory {
_directory ??= entryDirRepo.getOrCreate(path != null ? pContext.dirname(path!) : null);
return _directory!.resolved;
}
String? get filenameWithoutExtension {
_filename ??= path != null ? pContext.basenameWithoutExtension(path!) : null;
return _filename;
}
// file extension, including the `.`
String? get extension {
_extension ??= path != null ? pContext.extension(path!) : null;
return _extension;
}
// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
@override
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
bool get isCatalogued => _catalogMetadata != null;
DateTime? _bestDate;
DateTime? get bestDate {
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ??
dateTimeFromMillis(sourceDateTakenMillis) ??
dateTimeFromMillis(dateModifiedMillis ?? 0);
return _bestDate;
}
@override
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isHdr => _catalogMetadata?.isHdr ?? false;
int get rating => _catalogMetadata?.rating ?? 0;
bool get isRotated => rotationDegrees % 180 == 90;
String? get sourceTitle => _sourceTitle;
set sourceTitle(String? sourceTitle) {
_sourceTitle = sourceTitle;
_bestTitle = null;
}
int? get dateModifiedMillis => _dateModifiedMillis;
set dateModifiedMillis(int? dateModifiedMillis) {
_dateModifiedMillis = dateModifiedMillis;
_bestDate = null;
}
DateTime? get monthTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month);
}
DateTime? get dayTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month, d.day);
}
@override
int? get durationMillis => _durationMillis;
set durationMillis(int? durationMillis) {
_durationMillis = durationMillis;
_durationText = null;
}
String? _durationText;
String get durationText {
_durationText ??= formatFriendlyDuration(Duration(milliseconds: durationMillis ?? 0));
return _durationText!;
}
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
bool get hasAddress => _addressDetails != null;
bool get hasFineAddress =>
_addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
Set<String>? _tags;
Set<String> get tags {
_tags ??= _catalogMetadata?.xmpSubjects?.split(';').where((tag) => tag.isNotEmpty).toSet() ?? {};
return _tags!;
}
String? _bestTitle;
@override
String? get bestTitle {
_bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true
? _catalogMetadata!.xmpTitle
: (filenameWithoutExtension ?? sourceTitle);
return _bestTitle;
}
int? get catalogDateMillis => _catalogDateMillis;
set catalogDateMillis(int? dateMillis) {
_catalogDateMillis = dateMillis;
_bestDate = null;
}
CatalogMetadata? get catalogMetadata => _catalogMetadata;
set catalogMetadata(CatalogMetadata? newMetadata) {
final oldMimeType = mimeType;
final oldDateModifiedMillis = dateModifiedMillis;
final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped;
catalogDateMillis = newMetadata?.dateMillis;
_catalogMetadata = newMetadata;
_bestTitle = null;
_tags = null;
metadataChangeNotifier.notify();
_onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
}
void clearMetadata() {
catalogMetadata = null;
addressDetails = null;
}
AddressDetails? get addressDetails => _addressDetails;
set addressDetails(AddressDetails? newAddress) {
_addressDetails = newAddress;
addressChangeNotifier.notify();
}
String get shortAddress {
return {
_addressDetails?.countryName,
_addressDetails?.adminArea,
_addressDetails?.locality,
}.nonNulls.where((v) => v.isNotEmpty).join(', ');
}
static void normalizeMimeTypeFields(Map fields) {
final mimeType = fields[EntryFields.mimeType] as String?;
if (mimeType != null) {
fields[EntryFields.mimeType] = MimeTypes.normalize(mimeType);
}
final sourceMimeType = fields[EntryFields.sourceMimeType] as String?;
if (sourceMimeType != null) {
fields[EntryFields.sourceMimeType] = MimeTypes.normalize(sourceMimeType);
}
}
Future<void> applyNewFields(Map newFields, {required bool persist}) async {
final oldMimeType = mimeType;
final oldDateModifiedMillis = this.dateModifiedMillis;
final oldRotationDegrees = this.rotationDegrees;
final oldIsFlipped = this.isFlipped;
final uri = newFields[EntryFields.uri];
if (uri is String) this.uri = uri;
final path = newFields[EntryFields.path];
if (path is String) this.path = path;
final contentId = newFields[EntryFields.contentId];
if (contentId is int) this.contentId = contentId;
final sourceTitle = newFields[EntryFields.title];
if (sourceTitle is String) this.sourceTitle = sourceTitle;
final sourceRotationDegrees = newFields[EntryFields.sourceRotationDegrees];
if (sourceRotationDegrees is int) this.sourceRotationDegrees = sourceRotationDegrees;
final sourceDateTakenMillis = newFields[EntryFields.sourceDateTakenMillis];
if (sourceDateTakenMillis is int) this.sourceDateTakenMillis = sourceDateTakenMillis;
final width = newFields[EntryFields.width];
if (width is int) this.width = width;
final height = newFields[EntryFields.height];
if (height is int) this.height = height;
final durationMillis = newFields[EntryFields.durationMillis];
if (durationMillis is int) this.durationMillis = durationMillis;
final sizeBytes = newFields[EntryFields.sizeBytes];
if (sizeBytes is int) this.sizeBytes = sizeBytes;
final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
final rotationDegrees = newFields[EntryFields.rotationDegrees];
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
final isFlipped = newFields[EntryFields.isFlipped];
if (isFlipped is bool) this.isFlipped = isFlipped;
if (persist) {
await localMediaDb.updateEntry(id, this);
if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
}
await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
metadataChangeNotifier.notify();
}
Future<void> refresh({
required bool background,
required bool persist,
required Set<EntryDataType> dataTypes,
}) async {
_bestDate = null;
_bestTitle = null;
_tags = null;
if (persist) {
await localMediaDb.removeIds({id}, dataTypes: dataTypes);
}
final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
if (updatedEntry != null) {
await applyNewFields(updatedEntry.toDatabaseMap(), persist: persist);
}
}
Future<bool> delete() {
final opCompleter = Completer<bool>();
mediaEditService
.delete(entries: {this})
.listen(
(event) => opCompleter.complete(event.success && !event.skipped),
onError: opCompleter.completeError,
onDone: () {
if (!opCompleter.isCompleted) {
opCompleter.complete(false);
}
},
);
return opCompleter.future;
}
Future<void> _onVisualFieldChanged(
String oldMimeType,
int? oldDateModifiedMillis,
int oldRotationDegrees,
bool oldIsFlipped,
) async {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) ||
oldDateModifiedMillis != dateModifiedMillis ||
oldRotationDegrees != rotationDegrees ||
oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped, isAnimated);
visualChangeNotifier.notify();
}
}
// ------------------------------------------------------------
// Helpers “remote-friendly”
// ------------------------------------------------------------
static String? _inferMimeFromRemotePath(Map map) {
final path = (map['remotePath'] as String?) ?? (map[EntryFields.path] as String?);
if (path == null) return null;
final lower = path.toLowerCase();
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
if (lower.endsWith('.png')) return 'image/png';
if (lower.endsWith('.webp')) return 'image/webp';
if (lower.endsWith('.gif')) return 'image/gif';
if (lower.endsWith('.mp4')) return 'video/mp4';
if (lower.endsWith('.mov')) return 'video/quicktime';
if (lower.endsWith('.mkv')) return 'video/x-matroska';
return null;
}
static int? _syntheticContentId(Map map) {
final id = map[EntryFields.id] as int?;
if (id == null) return null;
return 1000000000 + id; // disgiunto dai locali, >0
}
}

View file

@ -0,0 +1,653 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:aves/model/entry/cache.dart';
import 'package:aves/model/entry/dirs.dart';
import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
enum EntryDataType { basic, aspectRatio, catalog, address, references }
class AvesEntry with AvesEntryBase {
// ============================================================
// CAMPI ORIGINALI AVES
// ============================================================
@override
int id;
@override
String uri;
@override
int? pageId;
@override
int? sizeBytes;
String? _path, _filename, _extension, _sourceTitle;
EntryDir? _directory;
int? contentId;
final String sourceMimeType;
int width, height, sourceRotationDegrees;
int? dateAddedSecs, _dateModifiedMillis, sourceDateTakenMillis, _durationMillis;
bool trashed;
int origin;
int? _catalogDateMillis;
CatalogMetadata? _catalogMetadata;
AddressDetails? _addressDetails;
TrashDetails? trashDetails;
List<AvesEntry>? stackedEntries;
@override
final AChangeNotifier visualChangeNotifier = AChangeNotifier();
final AChangeNotifier metadataChangeNotifier = AChangeNotifier(),
addressChangeNotifier = AChangeNotifier();
// ============================================================
// CAMPI REMOTI (AGGIUNTI DA TE)
// ============================================================
String? remoteId;
String? remotePath;
String? remoteThumb1;
String? remoteThumb2;
String? provider;
double? latitude;
double? longitude;
double? altitude;
int? remoteWidth;
int? remoteHeight;
int? remoteRotation;
// Getter utili
bool get isRemote => origin == 1;
String? get remoteThumb => remoteThumb2 ?? remoteThumb1;
// ============================================================
// COSTRUTTORE
// ============================================================
AvesEntry({
required int? id,
required this.uri,
required String? path,
required this.contentId,
required this.pageId,
required this.sourceMimeType,
required this.width,
required this.height,
required this.sourceRotationDegrees,
required this.sizeBytes,
required String? sourceTitle,
required this.dateAddedSecs,
required int? dateModifiedMillis,
required this.sourceDateTakenMillis,
required int? durationMillis,
required this.trashed,
required this.origin,
this.stackedEntries,
this.remoteId,
this.remotePath,
this.remoteThumb1,
this.remoteThumb2,
this.provider,
this.latitude,
this.longitude,
this.altitude,
this.remoteWidth,
this.remoteHeight,
this.remoteRotation,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated(
library: 'aves',
className: '$AvesEntry',
object: this,
);
}
this.path = path;
this.sourceTitle = sourceTitle;
this.dateModifiedMillis = dateModifiedMillis;
this.durationMillis = durationMillis;
}
// ============================================================
// COPY-WITH
// ============================================================
AvesEntry copyWith({
int? id,
String? uri,
String? path,
int? contentId,
String? title,
int? dateAddedSecs,
int? dateModifiedMillis,
int? origin,
List<AvesEntry>? stackedEntries,
}) {
final copyEntryId = id ?? this.id;
final copied = AvesEntry(
id: copyEntryId,
uri: uri ?? this.uri,
path: path ?? this.path,
contentId: contentId ?? this.contentId,
pageId: null,
sourceMimeType: sourceMimeType,
width: width,
height: height,
sourceRotationDegrees: sourceRotationDegrees,
sizeBytes: sizeBytes,
sourceTitle: title ?? sourceTitle,
dateAddedSecs: dateAddedSecs ?? this.dateAddedSecs,
dateModifiedMillis: dateModifiedMillis ?? this.dateModifiedMillis,
sourceDateTakenMillis: sourceDateTakenMillis,
durationMillis: durationMillis,
trashed: trashed,
origin: origin ?? this.origin,
stackedEntries: stackedEntries ?? this.stackedEntries,
// campi remoti copiati
remoteId: remoteId,
remotePath: remotePath,
remoteThumb1: remoteThumb1,
remoteThumb2: remoteThumb2,
provider: provider,
latitude: latitude,
longitude: longitude,
altitude: altitude,
)
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
..addressDetails = _addressDetails?.copyWith(id: copyEntryId)
..trashDetails = trashDetails?.copyWith(id: copyEntryId);
return copied;
}
// ============================================================
// FROM MAP (DB → MODEL)
// ============================================================
factory AvesEntry.fromMap(Map map) {
return AvesEntry(
id: map[EntryFields.id] as int?,
uri: (map[EntryFields.uri] as String?) ?? 'remote://missing',
path: map[EntryFields.path] as String?,
pageId: null,
contentId: map[EntryFields.contentId] as int?,
sourceMimeType: map[EntryFields.sourceMimeType] as String,
width: map[EntryFields.width] as int? ?? 0,
height: map[EntryFields.height] as int? ?? 0,
sourceRotationDegrees: map[EntryFields.sourceRotationDegrees] as int? ?? 0,
sizeBytes: map[EntryFields.sizeBytes] as int?,
sourceTitle: map[EntryFields.title] as String?,
dateAddedSecs: map[EntryFields.dateAddedSecs] as int?,
dateModifiedMillis: map[EntryFields.dateModifiedMillis] as int?,
sourceDateTakenMillis: map[EntryFields.sourceDateTakenMillis] as int?,
durationMillis: map[EntryFields.durationMillis] as int?,
trashed: (map[EntryFields.trashed] as int? ?? 0) != 0,
origin: map[EntryFields.origin] as int,
// --- REMOTE FIELDS ---
remoteId: map['remoteId'] as String?,
remotePath: map['remotePath'] as String?,
remoteThumb1: map['remoteThumb1'] as String?,
remoteThumb2: map['remoteThumb2'] as String?,
provider: map['provider'] as String?,
remoteWidth: map['remoteWidth'] as int?,
remoteHeight: map['remoteHeight'] as int?,
remoteRotation: map['remoteRotation'] as int?,
latitude: (map['latitude'] as num?)?.toDouble(),
longitude: (map['longitude'] as num?)?.toDouble(),
altitude: (map['altitude'] as num?)?.toDouble(),
);
}
// ============================================================
// TO MAP (MODEL → DB)
// ============================================================
Map<String, dynamic> toDatabaseMap() {
return {
EntryFields.id: id,
EntryFields.uri: uri,
EntryFields.path: path,
EntryFields.contentId: contentId,
EntryFields.sourceMimeType: sourceMimeType,
EntryFields.width: width,
EntryFields.height: height,
EntryFields.sourceRotationDegrees: sourceRotationDegrees,
EntryFields.sizeBytes: sizeBytes,
EntryFields.title: sourceTitle,
EntryFields.dateAddedSecs: dateAddedSecs,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sourceDateTakenMillis: sourceDateTakenMillis,
EntryFields.durationMillis: durationMillis,
EntryFields.trashed: trashed ? 1 : 0,
EntryFields.origin: origin,
// --- REMOTE FIELDS ---
'remoteId': remoteId,
'remotePath': remotePath,
'remoteThumb1': remoteThumb1,
'remoteThumb2': remoteThumb2,
'provider': provider,
'remoteWidth': remoteWidth,
'remoteHeight': remoteHeight,
'remoteRotation': remoteRotation,
'latitude': latitude,
'longitude': longitude,
'altitude': altitude,
};
}
// ============================================================
// (TUTTO IL RESTO È IDENTICO ALLA VERSIONE ORIGINALE AVES)
// ============================================================
// ... (qui rimane invariato tutto il codice originale che avevi già)
Map<String, dynamic> toPlatformEntryMap() {
return {
EntryFields.uri: uri,
EntryFields.path: path,
EntryFields.pageId: pageId,
EntryFields.mimeType: mimeType,
EntryFields.width: width,
EntryFields.height: height,
EntryFields.rotationDegrees: rotationDegrees,
EntryFields.isFlipped: isFlipped,
EntryFields.dateModifiedMillis: dateModifiedMillis,
EntryFields.sizeBytes: sizeBytes,
EntryFields.trashed: trashed,
EntryFields.trashPath: trashDetails?.path,
EntryFields.origin: origin,
};
}
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectDisposed(object: this);
}
visualChangeNotifier.dispose();
metadataChangeNotifier.dispose();
addressChangeNotifier.dispose();
}
// do not implement [Object.==] and [Object.hashCode] using mutable attributes (e.g. `uri`)
// so that we can reliably use instances in a `Set`, which requires consistent hash codes over time
@override
String toString() => '$runtimeType#${shortHash(this)}{id=$id, uri=$uri, path=$path, pageId=$pageId}';
set path(String? path) {
_path = path;
_directory = null;
_filename = null;
_extension = null;
_bestTitle = null;
}
@override
String? get path => _path;
// directory path, without the trailing separator
String? get directory {
_directory ??= entryDirRepo.getOrCreate(path != null ? pContext.dirname(path!) : null);
return _directory!.resolved;
}
String? get filenameWithoutExtension {
_filename ??= path != null ? pContext.basenameWithoutExtension(path!) : null;
return _filename;
}
// file extension, including the `.`
String? get extension {
_extension ??= path != null ? pContext.extension(path!) : null;
return _extension;
}
// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
@override
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
bool get isCatalogued => _catalogMetadata != null;
DateTime? _bestDate;
DateTime? get bestDate {
_bestDate ??= dateTimeFromMillis(_catalogDateMillis) ?? dateTimeFromMillis(sourceDateTakenMillis) ?? dateTimeFromMillis(dateModifiedMillis ?? 0);
return _bestDate;
}
@override
bool get isAnimated => catalogMetadata?.isAnimated ?? false;
bool get isHdr => _catalogMetadata?.isHdr ?? false;
int get rating => _catalogMetadata?.rating ?? 0;
// Patch1
// @override
// int get rotationDegrees => _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
@override
int get rotationDegrees {
if (isRemote && remoteRotation != null) {
return remoteRotation!;
}
return _catalogMetadata?.rotationDegrees ?? sourceRotationDegrees;
}
// Patch1
// set rotationDegrees(int rotationDegrees) {
// sourceRotationDegrees = rotationDegrees;
// _catalogMetadata?.rotationDegrees = rotationDegrees;
// }
@override
set rotationDegrees(int rotationDegrees) {
if (isRemote) {
remoteRotation = rotationDegrees;
} else {
sourceRotationDegrees = rotationDegrees;
_catalogMetadata?.rotationDegrees = rotationDegrees;
}
}
//
bool get isFlipped => _catalogMetadata?.isFlipped ?? false;
set isFlipped(bool isFlipped) => _catalogMetadata?.isFlipped = isFlipped;
// Media Store size/rotation is inaccurate, e.g. a portrait FHD video is rotated according to its metadata,
// so it should be registered as width=1920, height=1080, orientation=90,
// but is incorrectly registered as width=1080, height=1920, orientation=0.
// Double-checking the width/height during loading or cataloguing is the proper solution, but it would take space and time.
// Comparing width and height can help with the portrait FHD video example,
// but it fails for a portrait screenshot rotated, which is landscape with width=1080, height=1920, orientation=90
bool get isRotated => rotationDegrees % 180 == 90;
// Patch2
// @override
// double get displayAspectRatio {
// if (width == 0 || height == 0) return 1;
// return isRotated ? height / width : width / height;
// }
@override
double get displayAspectRatio {
double w = width.toDouble();
double h = height.toDouble();
if (isRemote && remoteWidth != null && remoteHeight != null) {
w = remoteWidth!.toDouble();
h = remoteHeight!.toDouble();
}
if (w == 0 || h == 0) return 1;
return isRotated ? h / w : w / h;
}
//
@override
Size get displaySize {
// PATCH: dimensioni corrette per immagini remote
if (isRemote && remoteWidth != null && remoteHeight != null) {
final w = remoteWidth!.toDouble();
final h = remoteHeight!.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
// fallback originale Aves
final w = width.toDouble();
final h = height.toDouble();
return isRotated ? Size(h, w) : Size(w, h);
}
String? get sourceTitle => _sourceTitle;
set sourceTitle(String? sourceTitle) {
_sourceTitle = sourceTitle;
_bestTitle = null;
}
int? get dateModifiedMillis => _dateModifiedMillis;
set dateModifiedMillis(int? dateModifiedMillis) {
_dateModifiedMillis = dateModifiedMillis;
_bestDate = null;
}
// TODO TLAD cache _monthTaken
DateTime? get monthTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month);
}
// TODO TLAD cache _dayTaken
DateTime? get dayTaken {
final d = bestDate;
return d == null ? null : DateTime(d.year, d.month, d.day);
}
@override
int? get durationMillis => _durationMillis;
set durationMillis(int? durationMillis) {
_durationMillis = durationMillis;
_durationText = null;
}
String? _durationText;
String get durationText {
_durationText ??= formatFriendlyDuration(Duration(milliseconds: durationMillis ?? 0));
return _durationText!;
}
// returns whether this entry has GPS coordinates
// (0, 0) coordinates are considered invalid, as it is likely a default value
bool get hasGps => (_catalogMetadata?.latitude ?? 0) != 0 || (_catalogMetadata?.longitude ?? 0) != 0;
bool get hasAddress => _addressDetails != null;
// has a place, or at least the full country name
// derived from Google reverse geocoding addresses
bool get hasFineAddress => _addressDetails?.place?.isNotEmpty == true || (_addressDetails?.countryName?.length ?? 0) > 3;
Set<String>? _tags;
Set<String> get tags {
_tags ??= _catalogMetadata?.xmpSubjects?.split(';').where((tag) => tag.isNotEmpty).toSet() ?? {};
return _tags!;
}
String? _bestTitle;
@override
String? get bestTitle {
_bestTitle ??= _catalogMetadata?.xmpTitle?.isNotEmpty == true ? _catalogMetadata!.xmpTitle : (filenameWithoutExtension ?? sourceTitle);
return _bestTitle;
}
int? get catalogDateMillis => _catalogDateMillis;
set catalogDateMillis(int? dateMillis) {
_catalogDateMillis = dateMillis;
_bestDate = null;
}
CatalogMetadata? get catalogMetadata => _catalogMetadata;
set catalogMetadata(CatalogMetadata? newMetadata) {
final oldMimeType = mimeType;
final oldDateModifiedMillis = dateModifiedMillis;
final oldRotationDegrees = rotationDegrees;
final oldIsFlipped = isFlipped;
catalogDateMillis = newMetadata?.dateMillis;
_catalogMetadata = newMetadata;
_bestTitle = null;
_tags = null;
metadataChangeNotifier.notify();
_onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
}
void clearMetadata() {
catalogMetadata = null;
addressDetails = null;
}
AddressDetails? get addressDetails => _addressDetails;
set addressDetails(AddressDetails? newAddress) {
_addressDetails = newAddress;
addressChangeNotifier.notify();
}
String get shortAddress {
// `admin area` examples: Seoul, Geneva, null
// `locality` examples: Mapo-gu, Geneva, Annecy
return {
_addressDetails?.countryName,
_addressDetails?.adminArea,
_addressDetails?.locality,
}.nonNulls.where((v) => v.isNotEmpty).join(', ');
}
static void normalizeMimeTypeFields(Map fields) {
final mimeType = fields[EntryFields.mimeType] as String?;
if (mimeType != null) {
fields[EntryFields.mimeType] = MimeTypes.normalize(mimeType);
}
final sourceMimeType = fields[EntryFields.sourceMimeType] as String?;
if (sourceMimeType != null) {
fields[EntryFields.sourceMimeType] = MimeTypes.normalize(sourceMimeType);
}
}
Future<void> applyNewFields(Map newFields, {required bool persist}) async {
final oldMimeType = mimeType;
final oldDateModifiedMillis = this.dateModifiedMillis;
final oldRotationDegrees = this.rotationDegrees;
final oldIsFlipped = this.isFlipped;
final uri = newFields[EntryFields.uri];
if (uri is String) this.uri = uri;
final path = newFields[EntryFields.path];
if (path is String) this.path = path;
final contentId = newFields[EntryFields.contentId];
if (contentId is int) this.contentId = contentId;
final sourceTitle = newFields[EntryFields.title];
if (sourceTitle is String) this.sourceTitle = sourceTitle;
final sourceRotationDegrees = newFields[EntryFields.sourceRotationDegrees];
if (sourceRotationDegrees is int) this.sourceRotationDegrees = sourceRotationDegrees;
final sourceDateTakenMillis = newFields[EntryFields.sourceDateTakenMillis];
if (sourceDateTakenMillis is int) this.sourceDateTakenMillis = sourceDateTakenMillis;
final width = newFields[EntryFields.width];
if (width is int) this.width = width;
final height = newFields[EntryFields.height];
if (height is int) this.height = height;
final durationMillis = newFields[EntryFields.durationMillis];
if (durationMillis is int) this.durationMillis = durationMillis;
final sizeBytes = newFields[EntryFields.sizeBytes];
if (sizeBytes is int) this.sizeBytes = sizeBytes;
final dateModifiedMillis = newFields[EntryFields.dateModifiedMillis];
if (dateModifiedMillis is int) this.dateModifiedMillis = dateModifiedMillis;
final rotationDegrees = newFields[EntryFields.rotationDegrees];
if (rotationDegrees is int) this.rotationDegrees = rotationDegrees;
final isFlipped = newFields[EntryFields.isFlipped];
if (isFlipped is bool) this.isFlipped = isFlipped;
if (persist) {
await localMediaDb.updateEntry(id, this);
if (catalogMetadata != null) await localMediaDb.saveCatalogMetadata({catalogMetadata!});
}
await _onVisualFieldChanged(oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped);
metadataChangeNotifier.notify();
}
Future<void> refresh({
required bool background,
required bool persist,
required Set<EntryDataType> dataTypes,
}) async {
// clear derived fields
_bestDate = null;
_bestTitle = null;
_tags = null;
if (persist) {
await localMediaDb.removeIds({id}, dataTypes: dataTypes);
}
final updatedEntry = await mediaFetchService.getEntry(uri, mimeType);
if (updatedEntry != null) {
await applyNewFields(updatedEntry.toDatabaseMap(), persist: persist);
}
}
Future<bool> delete() {
final opCompleter = Completer<bool>();
mediaEditService
.delete(entries: {this})
.listen(
(event) => opCompleter.complete(event.success && !event.skipped),
onError: opCompleter.completeError,
onDone: () {
if (!opCompleter.isCompleted) {
opCompleter.complete(false);
}
},
);
return opCompleter.future;
}
// when the MIME type or the image itself changed (e.g. after rotation)
Future<void> _onVisualFieldChanged(
String oldMimeType,
int? oldDateModifiedMillis,
int oldRotationDegrees,
bool oldIsFlipped,
) async {
if ((!MimeTypes.refersToSameType(oldMimeType, mimeType) && !MimeTypes.isVideo(oldMimeType)) || oldDateModifiedMillis != dateModifiedMillis || oldRotationDegrees != rotationDegrees || oldIsFlipped != isFlipped) {
await EntryCache.evict(uri, oldMimeType, oldDateModifiedMillis, oldRotationDegrees, oldIsFlipped, isAnimated);
visualChangeNotifier.notify();
}
}
}

View file

@ -11,6 +11,9 @@ import 'package:flutter/painting.dart';
import 'package:aves/remote/remote_http.dart';
extension ExtraAvesEntryImages on AvesEntry {
// === Helper per remoti ===
bool get isRemote => origin == 1;
bool isThumbnailReady({double extent = 0}) => _isReady(_getThumbnailProviderKey(extent));
ThumbnailProvider getThumbnail({double extent = 0}) {
@ -31,54 +34,60 @@ extension ExtraAvesEntryImages on AvesEntry {
);
}
RegionProvider getRegion({int sampleSize = 1, double scale = 1, required Rectangle<num> region}) {
if (isRemote) {
// Non supportiamo il tiling di region su immagini remote (non cè file locale).
throw UnsupportedError("Region tiling not supported for remote images");
}
RegionProvider getRegion({int sampleSize = 1, double scale = 1, required Rectangle<num> region}) {
if (isRemote) {
throw UnsupportedError("Region tiling not supported for remote images");
}
return RegionProvider(
RegionProviderKey(
uri: uri,
mimeType: mimeType,
pageId: pageId,
sizeBytes: sizeBytes,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
sampleSize: sampleSize,
regionRect: Rectangle(
(region.left * scale).round(),
(region.top * scale).round(),
(region.width * scale).round(),
(region.height * scale).round(),
return RegionProvider(
RegionProviderKey(
uri: uri,
mimeType: mimeType,
pageId: pageId,
sizeBytes: sizeBytes,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
sampleSize: sampleSize,
regionRect: Rectangle(
(region.left * scale).round(),
(region.top * scale).round(),
(region.width * scale).round(),
(region.height * scale).round(),
),
imageSize: Size((width * scale).toDouble(), (height * scale).toDouble()),
),
imageSize: Size((width * scale).toDouble(), (height * scale).toDouble()),
),
);
}
);
}
Rectangle<double> get fullImageRegion => Rectangle<double>(.0, .0, width.toDouble(), height.toDouble());
/// Provider dellimmagine completa.
/// - Remoto `NetworkImage` con header Bearer sincroni (se già in cache via RemoteHttp.headers()).
/// - Locale `FullImage` (pipeline classica Aves).
ImageProvider get fullImage {
if (isRemote) {
final abs = RemoteHttp.absUrl(remotePath ?? path);
// Se la cache non è ancora popolata, può essere null: chi usa il ramo viewer remoto fa comunque await degli headers.
final hdrs = RemoteHttp.peekHeaders();
return NetworkImage(abs, headers: hdrs);
}
ImageProvider get fullImage {
if (isRemote) {
return NetworkImage(RemoteHttp.absUrl(remotePath!));
return FullImage(
uri: uri,
mimeType: mimeType,
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
isAnimated: isAnimated,
sizeBytes: sizeBytes,
);
}
return FullImage(
uri: uri,
mimeType: mimeType,
pageId: pageId,
rotationDegrees: rotationDegrees,
isFlipped: isFlipped,
isAnimated: isAnimated,
sizeBytes: sizeBytes,
);
}
bool _isReady(Object providerKey) => imageCache.statusForKey(providerKey).keepAlive;
List<ThumbnailProvider> get cachedThumbnails => EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
List<ThumbnailProvider> get cachedThumbnails =>
EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).where(_isReady).map(ThumbnailProvider.new).toList();
ThumbnailProvider get bestCachedThumbnail {
final sizedThumbnailKey = EntryCache.thumbnailRequestExtents.map(_getThumbnailProviderKey).firstWhereOrNull(_isReady);

View file

@ -1,3 +1,4 @@
// lib/model/source/collection_source.dart
import 'dart:async';
import 'dart:ui';
@ -40,6 +41,9 @@ import 'package:leak_tracker/leak_tracker.dart';
typedef SourceScope = Set<CollectionFilter>?;
// Trace opzionale: mostra chi nasconderebbe i remoti (non li nascondiamo)
const bool kTraceHiddenRemotes = true;
mixin SourceBase {
EventBus get eventBus;
@ -156,7 +160,22 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
TrashFilter.instance,
..._getAppHiddenFilters(),
};
return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
// PATCH B: non nascondere i remoti con i filtri "nascosti" (tranne Trash)
return entries.where((entry) {
if (entry.origin == 1) {
if (kTraceHiddenRemotes) {
final hiddenBy = hiddenFilters.firstWhereOrNull((f) => f.test(entry));
if (hiddenBy != null && !TrashFilter.instance.test(entry)) {
debugPrint('[hidden][trace] remote id=${entry.id} rid=${entry.remoteId} by=${hiddenBy.runtimeType}');
}
}
// remoti: nascondi solo se nel cestino
return !TrashFilter.instance.test(entry);
}
// locali: logica originale
return !hiddenFilters.any((filter) => filter.test(entry));
});
}
Iterable<AvesEntry> _applyTrashFilter(Iterable<AvesEntry> entries) {

View file

@ -0,0 +1,659 @@
import 'dart:async';
import 'dart:ui';
import 'package:aves/model/covers.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/catalog.dart';
import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/container/album_group.dart';
import 'package:aves/model/filters/container/tag_group.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/grouping/convert.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location/country.dart';
import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/location/place.dart';
import 'package:aves/model/source/location/state.dart';
import 'package:aves/model/source/tag.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/analysis_service.dart';
import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';
typedef SourceScope = Set<CollectionFilter>?;
mixin SourceBase {
EventBus get eventBus;
Map<int, AvesEntry> get entryById;
Set<AvesEntry> get allEntries;
Set<AvesEntry> get visibleEntries;
Set<AvesEntry> get trashedEntries;
List<AvesEntry> get sortedEntriesByDate;
ValueNotifier<SourceState> stateNotifier = ValueNotifier(SourceState.ready);
set state(SourceState value) => stateNotifier.value = value;
SourceState get state => stateNotifier.value;
bool get isReady => state == SourceState.ready;
ValueNotifier<ProgressEvent> progressNotifier = ValueNotifier(const ProgressEvent(done: 0, total: 0));
void setProgress({required int done, required int total}) => progressNotifier.value = ProgressEvent(done: done, total: total);
void invalidateEntries();
}
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
static const fullScope = <CollectionFilter>{};
CollectionSource() {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated(
library: 'aves',
className: '$CollectionSource',
object: this,
);
}
settings.updateStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => invalidateStoredAlbumDisplayNames());
settings.updateStream.where((event) => event.key == SettingKeys.hiddenFiltersKey).listen((event) {
final oldValue = event.oldValue;
if (oldValue is List<String>?) {
final oldHiddenFilters = (oldValue ?? []).map(CollectionFilter.fromJson).nonNulls.toSet();
final newlyVisibleFilters = oldHiddenFilters.whereNot(settings.hiddenFilters.contains).toSet();
_onFilterVisibilityChanged(newlyVisibleFilters);
}
});
vaults.addListener(_onVaultsChanged);
}
@mustCallSuper
void dispose() {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectDisposed(object: this);
}
vaults.removeListener(_onVaultsChanged);
_rawEntries.forEach((v) => v.dispose());
}
set canAnalyze(bool enabled);
final EventBus _eventBus = EventBus();
@override
EventBus get eventBus => _eventBus;
final Map<int, AvesEntry> _entryById = {};
@override
Map<int, AvesEntry> get entryById => Map.unmodifiable(_entryById);
final Set<AvesEntry> _rawEntries = {};
@override
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
Set<AvesEntry>? _visibleEntries, _trashedEntries;
@override
Set<AvesEntry> get visibleEntries {
_visibleEntries ??= Set.unmodifiable(_applyHiddenFilters(_rawEntries));
return _visibleEntries!;
}
@override
Set<AvesEntry> get trashedEntries {
_trashedEntries ??= Set.unmodifiable(_applyTrashFilter(_rawEntries));
return _trashedEntries!;
}
List<AvesEntry>? _sortedEntriesByDate;
@override
List<AvesEntry> get sortedEntriesByDate {
_sortedEntriesByDate ??= List.unmodifiable(visibleEntries.toList()..sort(AvesEntrySort.compareByDate));
return _sortedEntriesByDate!;
}
// known date by entry ID
late Map<int?, int?> _savedDates;
Future<void> loadDates() async {
_savedDates = Map.unmodifiable(await localMediaDb.loadDates());
}
Set<CollectionFilter> _getAppHiddenFilters() => {
...settings.hiddenFilters,
...vaults.vaultDirectories.where(vaults.isLocked).map((v) => StoredAlbumFilter(v, null)),
};
Iterable<AvesEntry> _applyHiddenFilters(Iterable<AvesEntry> entries) {
final hiddenFilters = {
TrashFilter.instance,
..._getAppHiddenFilters(),
};
return entries.where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
}
Iterable<AvesEntry> _applyTrashFilter(Iterable<AvesEntry> entries) {
final hiddenFilters = _getAppHiddenFilters();
return entries.where(TrashFilter.instance.test).where((entry) => !hiddenFilters.any((filter) => filter.test(entry)));
}
void _invalidate({Set<AvesEntry>? entries, bool notify = true}) {
invalidateEntries();
invalidateAlbumFilterSummary(entries: entries, notify: notify);
invalidateCountryFilterSummary(entries: entries, notify: notify);
invalidatePlaceFilterSummary(entries: entries, notify: notify);
invalidateStateFilterSummary(entries: entries, notify: notify);
invalidateTagFilterSummary(entries: entries, notify: notify);
}
@override
void invalidateEntries() {
_visibleEntries = null;
_trashedEntries = null;
_sortedEntriesByDate = null;
}
void updateDerivedFilters([Set<AvesEntry>? entries]) {
_invalidate(entries: entries);
// it is possible for entries hidden by a filter type, to have an impact on other types
// e.g. given a sole entry for country C and tag T, hiding T should make C disappear too
updateDirectories();
updateLocations();
updateTags();
}
void addEntries(Set<AvesEntry> entries, {bool notify = true}) {
if (entries.isEmpty) return;
final newIdMapEntries = Map.fromEntries(entries.map((entry) => MapEntry(entry.id, entry)));
if (_rawEntries.isNotEmpty) {
final newIds = newIdMapEntries.keys.toSet();
_rawEntries.removeWhere((entry) => newIds.contains(entry.id));
}
entries.where((entry) => entry.catalogDateMillis == null).forEach((entry) {
entry.catalogDateMillis = _savedDates[entry.id];
});
_entryById.addAll(newIdMapEntries);
_rawEntries.addAll(entries);
_invalidate(entries: entries, notify: notify);
addDirectories(albums: _applyHiddenFilters(entries).map((entry) => entry.directory).toSet(), notify: notify);
if (notify) {
eventBus.fire(EntryAddedEvent(entries));
}
}
Future<void> removeEntries(Set<String> uris, {required bool includeTrash}) async {
if (uris.isEmpty) return;
final entries = _rawEntries.where((entry) => uris.contains(entry.uri)).toSet();
if (!includeTrash) {
entries.removeWhere(TrashFilter.instance.test);
}
if (entries.isEmpty) return;
final ids = entries.map((entry) => entry.id).toSet();
await favourites.removeIds(ids);
await covers.removeIds(ids);
await localMediaDb.removeIds(ids);
ids.forEach((id) => _entryById.remove);
_rawEntries.removeAll(entries);
updateDerivedFilters(entries);
eventBus.fire(EntryRemovedEvent(entries));
}
void clearEntries() {
_entryById.clear();
_rawEntries.clear();
_invalidate();
// do not update directories/locations/tags here
// as it could reset filter dependent settings (pins, bookmarks, etc.)
// caller should take care of updating these at the right time
}
/// Carica dal DB tutte le entry **remote** (`origin=1`) non cestinate
/// e le aggiunge alla sorgente corrente (evitando duplicati per ID).
///
/// 👉 Va chiamato **dopo** che la sorgente locale è stata inizializzata
/// (es. subito dopo `await source.init(...)` nel tuo `home_page.dart`).
Future<void> appendRemoteEntries({bool notify = true}) async {
try {
final remotes = await localMediaDb.loadEntries(origin: 1);
if (remotes.isEmpty) return;
// Manteniamo visibili solo quelli non cestinati
final visibleRemotes = remotes.where((e) => !e.trashed).toSet();
if (visibleRemotes.isEmpty) return;
// Merge usando la logica standard (aggiorna mappe, invalida, eventi, filtri, ecc.)
addEntries(visibleRemotes, notify: notify);
} catch (e, st) {
debugPrint('CollectionSource.appendRemoteEntries error: $e\n$st');
}
}
Future<void> _moveEntry(AvesEntry entry, Map newFields, {required bool persist}) async {
newFields.keys.forEach((key) {
final newValue = newFields[key];
switch (key) {
case EntryFields.contentId:
entry.contentId = newValue as int?;
case EntryFields.dateModifiedMillis:
// `dateModifiedMillis` changes when moving entries to another directory,
// but it does not change when renaming the containing directory
entry.dateModifiedMillis = newValue as int?;
case EntryFields.path:
entry.path = newValue as String?;
case EntryFields.title:
entry.sourceTitle = newValue as String?;
case EntryFields.trashed:
final trashed = newValue as bool;
entry.trashed = trashed;
entry.trashDetails = trashed
? TrashDetails(
id: entry.id,
path: newFields[EntryFields.trashPath] as String,
dateMillis: DateTime.now().millisecondsSinceEpoch,
)
: null;
case EntryFields.uri:
entry.uri = newValue as String;
case EntryFields.origin:
entry.origin = newValue as int;
}
});
if (entry.trashed) {
final trashPath = entry.trashDetails?.path;
if (trashPath != null) {
entry.contentId = null;
entry.uri = Uri.file(trashPath).toString();
} else {
debugPrint('failed to update uri from unknown trash path for uri=${entry.uri}');
}
}
if (persist) {
await covers.moveEntry(entry);
final id = entry.id;
await localMediaDb.updateEntry(id, entry);
await localMediaDb.updateCatalogMetadata(id, entry.catalogMetadata);
await localMediaDb.updateAddress(id, entry.addressDetails);
await localMediaDb.updateTrash(id, entry.trashDetails);
}
}
Future<void> renameStoredAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> entries, Set<MoveOpEvent> movedOps) async {
final oldFilter = StoredAlbumFilter(sourceAlbum, null);
final newFilter = StoredAlbumFilter(destinationAlbum, null);
final group = albumGrouping.getFilterParent(oldFilter);
final pinned = settings.pinnedFilters.contains(oldFilter);
if (vaults.isVault(sourceAlbum)) {
await vaults.rename(sourceAlbum, destinationAlbum);
}
final existingCover = covers.of(oldFilter);
await covers.set(
filter: newFilter,
entryId: existingCover?.$1,
packageName: existingCover?.$2,
color: existingCover?.$3,
);
renameNewAlbum(sourceAlbum, destinationAlbum);
await updateAfterMove(
todoEntries: entries,
moveType: MoveType.move,
destinationAlbums: {destinationAlbum},
movedOps: movedOps,
);
// update bookmark
final albumBookmarks = settings.drawerAlbumBookmarks;
if (albumBookmarks != null) {
final index = albumBookmarks.indexWhere((v) => v is StoredAlbumFilter && v.album == sourceAlbum);
if (index >= 0) {
albumBookmarks.removeAt(index);
albumBookmarks.insert(index, newFilter);
settings.drawerAlbumBookmarks = albumBookmarks;
}
}
// update group
if (group != null) {
final newFilterUri = GroupingConversion.filterToUri(newFilter);
if (newFilterUri != null) {
albumGrouping.addToGroup({newFilterUri}, group);
}
final oldFilterUri = GroupingConversion.filterToUri(oldFilter);
if (oldFilterUri != null) {
albumGrouping.addToGroup({oldFilterUri}, null);
}
}
// restore pin, as the obsolete album got removed and its associated state cleaned
if (pinned) {
settings.pinnedFilters = settings.pinnedFilters
..remove(oldFilter)
..add(newFilter);
}
}
Future<void> updateAfterMove({
required Set<AvesEntry> todoEntries,
required MoveType moveType,
required Set<String> destinationAlbums,
required Set<MoveOpEvent> movedOps,
}) async {
if (movedOps.isEmpty) return;
final replacedUris = movedOps
.map((movedOp) => movedOp.newFields[EntryFields.path] as String?)
.map((targetPath) {
final existingEntry = _rawEntries.firstWhereOrNull((entry) => entry.path == targetPath && !entry.trashed);
return existingEntry?.uri;
})
.nonNulls
.toSet();
await removeEntries(replacedUris, includeTrash: false);
final fromAlbums = <String?>{};
final movedEntries = <AvesEntry>{};
final copy = moveType == MoveType.copy;
if (copy) {
movedOps.forEach((movedOp) {
final sourceUri = movedOp.uri;
final newFields = movedOp.newFields;
final sourceEntry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri);
if (sourceEntry != null) {
fromAlbums.add(sourceEntry.directory);
movedEntries.add(
sourceEntry.copyWith(
id: localMediaDb.nextId,
uri: newFields[EntryFields.uri] as String?,
path: newFields[EntryFields.path] as String?,
contentId: newFields[EntryFields.contentId] as int?,
// title can change when moved files are automatically renamed to avoid conflict
title: newFields[EntryFields.title] as String?,
dateAddedSecs: newFields[EntryFields.dateAddedSecs] as int?,
dateModifiedMillis: newFields[EntryFields.dateModifiedMillis] as int?,
origin: newFields[EntryFields.origin] as int?,
),
);
} else {
debugPrint('failed to find source entry with uri=$sourceUri');
}
});
await localMediaDb.insertEntries(movedEntries);
await localMediaDb.saveCatalogMetadata(movedEntries.map((entry) => entry.catalogMetadata).nonNulls.toSet());
await localMediaDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails).nonNulls.toSet());
} else {
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
final newFields = movedOp.newFields;
if (newFields.isNotEmpty) {
final sourceUri = movedOp.uri;
final entry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri);
if (entry != null) {
if (moveType == MoveType.fromBin) {
newFields[EntryFields.trashed] = false;
} else {
fromAlbums.add(entry.directory);
}
movedEntries.add(entry);
await _moveEntry(entry, newFields, persist: true);
}
}
});
}
switch (moveType) {
case MoveType.copy:
addEntries(movedEntries);
case MoveType.move:
case MoveType.export:
cleanEmptyAlbums(fromAlbums.nonNulls.toSet());
addDirectories(albums: destinationAlbums);
case MoveType.toBin:
case MoveType.fromBin:
updateDerivedFilters(movedEntries);
}
invalidateAlbumFilterSummary(directories: fromAlbums);
_invalidate(entries: movedEntries);
eventBus.fire(EntryMovedEvent(moveType, movedEntries));
}
Future<void> updateAfterRename({
required Set<AvesEntry> todoEntries,
required Set<MoveOpEvent> movedOps,
required bool persist,
}) async {
if (movedOps.isEmpty) return;
final movedEntries = <AvesEntry>{};
await Future.forEach<MoveOpEvent>(movedOps, (movedOp) async {
final newFields = movedOp.newFields;
if (newFields.isNotEmpty) {
final sourceUri = movedOp.uri;
final entry = todoEntries.firstWhereOrNull((entry) => entry.uri == sourceUri);
if (entry != null) {
movedEntries.add(entry);
await _moveEntry(entry, newFields, persist: persist);
}
}
});
eventBus.fire(EntryMovedEvent(MoveType.move, movedEntries));
}
SourceScope get loadedScope;
SourceScope get targetScope;
Future<void> init({
required SourceScope scope,
AnalysisController? analysisController,
bool loadTopEntriesFirst = false,
});
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController});
Future<void> refreshEntries(Set<AvesEntry> entries, Set<EntryDataType> dataTypes) async {
const background = false;
const persist = true;
await Future.forEach(entries, (entry) async {
await entry.refresh(background: background, persist: persist, dataTypes: dataTypes);
});
if (dataTypes.contains(EntryDataType.aspectRatio)) {
onAspectRatioChanged();
}
if (dataTypes.contains(EntryDataType.catalog)) {
// explicit GC before cataloguing multiple items
await deviceService.requestGarbageCollection();
await Future.forEach(entries, (entry) async {
await entry.catalog(background: background, force: dataTypes.contains(EntryDataType.catalog), persist: persist);
await localMediaDb.updateCatalogMetadata(entry.id, entry.catalogMetadata);
});
onCatalogMetadataChanged();
}
if (dataTypes.contains(EntryDataType.address)) {
await Future.forEach(entries, (entry) async {
await entry.locate(background: background, force: dataTypes.contains(EntryDataType.address), geocoderLocale: settings.appliedLocale);
await localMediaDb.updateAddress(entry.id, entry.addressDetails);
});
onAddressMetadataChanged();
}
updateDerivedFilters(entries);
eventBus.fire(EntryRefreshedEvent(entries));
}
Future<void> analyze(AnalysisController? analysisController, {Set<AvesEntry>? entries}) async {
// not only visible entries, as hidden and vault items may be analyzed
final todoEntries = entries ?? allEntries;
final defaultAnalysisController = AnalysisController();
final _analysisController = analysisController ?? defaultAnalysisController;
final force = _analysisController.force;
if (!_analysisController.isStopping) {
var startAnalysisService = false;
if (_analysisController.canStartService && settings.canUseAnalysisService) {
// cataloguing
if (!startAnalysisService) {
final opCount = (force ? todoEntries : todoEntries.where(TagMixin.catalogEntriesTest)).length;
startAnalysisService = opCount > TagMixin.commitCountThreshold;
}
// ignore locating countries
// locating places
if (!startAnalysisService && await availability.canLocatePlaces) {
final opCount = (force ? todoEntries.where((entry) => entry.hasGps) : todoEntries.where(LocationMixin.locatePlacesTest)).length;
startAnalysisService = opCount > LocationMixin.commitCountThreshold;
}
}
debugPrint('analyze ${todoEntries.length} entries, force=$force, starting service=$startAnalysisService');
if (startAnalysisService) {
final lifecycleState = AvesApp.lifecycleStateNotifier.value;
switch (lifecycleState) {
case AppLifecycleState.resumed:
case AppLifecycleState.inactive:
await AnalysisService.startService(
force: force,
entryIds: entries?.map((entry) => entry.id).toList(),
);
default:
unawaited(reportService.log('analysis service not started because app is in state=$lifecycleState'));
}
} else {
// explicit GC before cataloguing multiple items
await deviceService.requestGarbageCollection();
await catalogEntries(_analysisController, todoEntries);
updateDerivedFilters(todoEntries);
await locateEntries(_analysisController, todoEntries);
updateDerivedFilters(todoEntries);
}
}
defaultAnalysisController.dispose();
state = SourceState.ready;
}
void onAspectRatioChanged() => eventBus.fire(AspectRatioChangedEvent());
// monitoring
bool _canRefresh = true;
void pauseMonitoring() => _canRefresh = false;
void resumeMonitoring() => _canRefresh = true;
bool get canRefresh => _canRefresh;
// filter summary
int count(CollectionFilter filter) {
switch (filter) {
case AlbumBaseFilter _:
return albumEntryCount(filter);
case LocationFilter(level: LocationLevel.country):
return countryEntryCount(filter);
case LocationFilter(level: LocationLevel.state):
return stateEntryCount(filter);
case LocationFilter(level: LocationLevel.place):
return placeEntryCount(filter);
case TagBaseFilter _:
return tagEntryCount(filter);
}
return 0;
}
int size(CollectionFilter filter) {
switch (filter) {
case AlbumBaseFilter _:
return albumSize(filter);
case LocationFilter(level: LocationLevel.country):
return countrySize(filter);
case LocationFilter(level: LocationLevel.state):
return stateSize(filter);
case LocationFilter(level: LocationLevel.place):
return placeSize(filter);
case TagBaseFilter _:
return tagSize(filter);
}
return 0;
}
AvesEntry? recentEntry(CollectionFilter filter) {
switch (filter) {
case AlbumBaseFilter _:
return albumRecentEntry(filter);
case LocationFilter(level: LocationLevel.country):
return countryRecentEntry(filter);
case LocationFilter(level: LocationLevel.state):
return stateRecentEntry(filter);
case LocationFilter(level: LocationLevel.place):
return placeRecentEntry(filter);
case TagBaseFilter _:
return tagRecentEntry(filter);
}
return null;
}
AvesEntry? coverEntry(CollectionFilter filter) {
final id = covers.of(filter)?.$1;
if (id != null) {
final entry = visibleEntries.firstWhereOrNull((entry) => entry.id == id);
if (entry != null) return entry;
}
return recentEntry(filter);
}
void _onFilterVisibilityChanged(Set<CollectionFilter> newlyVisibleFilters) {
updateDerivedFilters();
eventBus.fire(const FilterVisibilityChangedEvent());
if (newlyVisibleFilters.isNotEmpty) {
final candidateEntries = visibleEntries.where((entry) => newlyVisibleFilters.any((f) => f.test(entry))).toSet();
analyze(null, entries: candidateEntries);
}
}
void _onVaultsChanged() {
final newlyVisibleFilters = vaults.vaultDirectories.whereNot(vaults.isLocked).map((v) => StoredAlbumFilter(v, null)).toSet();
_onFilterVisibilityChanged(newlyVisibleFilters);
}
}
class AspectRatioChangedEvent {}

View file

@ -1,3 +1,5 @@
// lib/model/source/media_store_source.dart
import 'dart:async';
import 'package:aves/model/covers.dart';
@ -18,6 +20,10 @@ import 'package:aves/utils/debouncer.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:sqflite/sqflite.dart' show Sqflite;
// AGGIUNTA: definizione origine remota
const int ORIGIN_REMOTE = 1;
class MediaStoreSource extends CollectionSource {
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
@ -74,7 +80,8 @@ class MediaStoreSource extends CollectionSource {
final deviceOffset = DateTime.now().timeZoneOffset.inMilliseconds;
final catalogOffset = settings.catalogTimeZoneOffsetMillis;
if (deviceOffset != catalogOffset) {
unawaited(reportService.log('Time zone offset change: $catalogOffset -> $deviceOffset. Clear catalog metadata to get correct date/times.'));
unawaited(reportService.log(
'Time zone offset change: $catalogOffset -> $deviceOffset. Clear catalog metadata to get correct date/times.'));
await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneOffsetMillis = deviceOffset;
@ -90,10 +97,31 @@ class MediaStoreSource extends CollectionSource {
unawaited(reportService.log('$runtimeType load (known) start'));
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
clearEntries();
final scopeAlbumFilters = _targetScope?.whereType<StoredAlbumFilter>();
final scopeDirectory = scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
final scopeDirectory =
scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
// 🔒 Sentinella: conteggio remoti PRIMA
final preRem = Sqflite.firstIntValue(
await localMediaDb.rawDb.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=$ORIGIN_REMOTE'),
) ??
0;
debugPrint('[cleanup][pre] remoti in DB = $preRem');
// 🔧 PATCH: cancella SOLO i locali
final swClear = Stopwatch()..start();
final deletedLocal = await localMediaDb.rawDb
.rawDelete('DELETE FROM entry WHERE origin = ?', [EntryOrigins.mediaStoreContent]);
swClear.stop();
debugPrint('$runtimeType load ${swClear.elapsed} clear local entries deleted $deletedLocal rows');
// 🔒 Sentinella: conteggio remoti DOPO
final postRem = Sqflite.firstIntValue(
await localMediaDb.rawDb.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=$ORIGIN_REMOTE'),
) ??
0;
debugPrint('[cleanup][post] remoti in DB = $postRem (Δ=${postRem - preRem})');
final Set<AvesEntry> topEntries = {};
if (loadTopEntriesFirst) {
@ -106,20 +134,24 @@ class MediaStoreSource extends CollectionSource {
}
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
final knownEntries =
await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
final isLargeCollection = knownEntries.length > 80000;
if (isLargeCollection && settings.isErrorReportingAllowed) {
// disable error reporting for large collections, to prevent noisy OOM reports
settings.isErrorReportingAllowed = false;
}
unawaited(reportService.setCustomKey('is_large_collection', isLargeCollection));
unawaited(reportService.log('$runtimeType found ${knownEntries.length} known entries'));
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
final knownDateByContentId =
Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
debugPrint('[media_store_source] removedContentIds count=${removedContentIds.length} sample=${removedContentIds.take(10)}');
if (topEntries.isNotEmpty) {
final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
@ -127,13 +159,17 @@ class MediaStoreSource extends CollectionSource {
final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet();
knownEntries.removeAll(removedEntries);
// PATCH: rimuovi solo locali, non remote
final localRemovedEntries = removedEntries.where((e) => e.origin != ORIGIN_REMOTE).toSet();
debugPrint('[media_store_source] removedEntries total=${removedEntries.length} localRemovedEntries=${localRemovedEntries.length}');
if (localRemovedEntries.isNotEmpty) {
await localMediaDb.removeIds(localRemovedEntries.map((entry) => entry.id).toSet());
}
// show known entries
debugPrint('$runtimeType load ${stopwatch.elapsed} add known entries');
// add entries without notifying, so that the collection is not refreshed
// with items that may be hidden right away because of their metadata
addEntries(knownEntries, notify: false);
// but use album notification without waiting for cataloguing
// so that it is more reactive when picking an album in view mode
notifyAlbumsChanged();
await _loadVaultEntries(scopeDirectory);
@ -147,7 +183,6 @@ class MediaStoreSource extends CollectionSource {
await loadCatalogMetadata();
await loadAddresses();
// trash
await loadTrashDetails();
unawaited(
deleteExpiredTrash().then(
@ -157,23 +192,14 @@ class MediaStoreSource extends CollectionSource {
removeEntries(deletedUris, includeTrash: true);
}
},
onError: (error) => debugPrint('failed to evict expired trash error=$error'),
),
);
}
updateDerivedFilters();
// clean up obsolete entries
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} remove obsolete entries');
await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet());
}
_loadedScope = _targetScope;
unawaited(reportService.log('$runtimeType load (known) done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${removedEntries.length} removed'));
if (_canAnalyze) {
// it can discover new entries only if it can analyze them
await _loadNewEntries(
analysisController: analysisController,
directory: scopeDirectory,
@ -194,111 +220,79 @@ class MediaStoreSource extends CollectionSource {
unawaited(reportService.log('$runtimeType load (new) start'));
final stopwatch = Stopwatch()..start();
// items to add to the collection
final newEntries = <AvesEntry>{};
// recover untracked trash items
debugPrint('$runtimeType load ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
newEntries.addAll(await recoverUntrackedTrashItems());
}
// verify paths because some apps move files without updating their `last modified date`
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete paths');
final knownPathByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
// PATCH: escludi remote
final knownPathByContentId = Map.fromEntries(
knownLiveEntries
.where((entry) => entry.origin != ORIGIN_REMOTE)
.map((entry) => MapEntry(entry.contentId, entry.path)),
);
final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet();
movedContentIds.forEach((contentId) {
// make obsolete by resetting its modified date
knownDateByContentId[contentId] = 0;
});
// fetch new & modified entries
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
final knownContentIds = knownDateByContentId.keys.toSet();
mediaStoreService
.getEntries(knownDateByContentId, directory: directory)
.listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.
final contentId = entry.contentId;
final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null;
entry.id = existingEntry?.id ?? localMediaDb.nextId;
newEntries.add(entry);
setProgress(done: newEntries.length, total: 0);
},
onDone: () async {
if (newEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} save ${newEntries.length} new entries');
await localMediaDb.insertEntries(newEntries);
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
(entry) {
final contentId = entry.contentId;
final existingEntry = knownContentIds.contains(contentId)
? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId)
: null;
entry.id = existingEntry?.id ?? localMediaDb.nextId;
// TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => duplicateId == v.id);
}
}
newEntries.add(entry);
setProgress(done: newEntries.length, total: 0);
},
onDone: () async {
if (newEntries.isNotEmpty) {
await localMediaDb.insertEntries(newEntries);
// update trash details, if any
await Future.forEach(newEntries.where((v) => v.trashed), (entry) async {
final trashDetails = entry.trashDetails;
if (trashDetails != null) {
await localMediaDb.updateTrash(entry.id, trashDetails);
} else {
unawaited(reportService.recordError(Exception('Adding trashed entry but trash details are missing for entry=$entry')));
}
});
addEntries(newEntries);
// new entries include existing entries with obsolete paths
// so directories may be added, but also removed or simply have their content summary changed
invalidateAlbumFilterSummary();
updateDirectories();
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => v.id == duplicateId);
}
}
debugPrint('$runtimeType load ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
// not only visible entries, as hidden and vault items may be analyzed
analysisEntries = allEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
}
await analyze(analysisController, entries: analysisEntries);
addEntries(newEntries);
invalidateAlbumFilterSummary();
updateDirectories();
}
// the home page may not reflect the current derived filters
// as the initial addition of entries is silent,
// so we manually notify change for potential home screen filters
notifyAlbumsChanged();
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
analysisEntries = allEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
}
await analyze(analysisController, entries: analysisEntries);
unawaited(reportService.log('$runtimeType load (new) done in ${stopwatch.elapsed.inSeconds}s for ${newEntries.length} new entries'));
},
onError: (error) => debugPrint('$runtimeType stream error=$error'),
);
notifyAlbumsChanged();
},
);
}
// returns URIs to retry later. They could be URIs that are:
// 1) currently being processed during bulk move/deletion
// 2) registered in the Media Store but still being processed by their owner in a temporary location
// For example, when taking a picture with a Galaxy S10e default camera app, querying the Media Store
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (!canRefresh || _essentialLoader == null || !isReady) return changedUris;
state = SourceState.loading;
unawaited(reportService.log('$runtimeType refresh start for ${changedUris.length} uris'));
final changedUriByContentId = Map.fromEntries(
changedUris.map((uri) {
final pathSegments = Uri.parse(uri).pathSegments;
// e.g. URI `content://media/` has no path segment
if (pathSegments.isEmpty) return null;
final idString = pathSegments.last;
final contentId = int.tryParse(idString);
@ -307,31 +301,52 @@ class MediaStoreSource extends CollectionSource {
}).nonNulls,
);
// clean up obsolete entries
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(changedUriByContentId.keys.toList())).toSet();
final obsoleteContentIds =
(await mediaStoreService.checkObsoleteContentIds(changedUriByContentId.keys.toList())).toSet();
final obsoleteUris = obsoleteContentIds.map((contentId) => changedUriByContentId[contentId]).nonNulls.toSet();
await removeEntries(obsoleteUris, includeTrash: false);
// PATCH: rimuovi solo locali
final localObsoleteUris = <String>{};
for (final uri in obsoleteUris) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
if (existingEntry == null) {
final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
if (sourceEntry != null && sourceEntry.origin != ORIGIN_REMOTE) {
localObsoleteUris.add(uri);
}
} else {
if (existingEntry.origin != ORIGIN_REMOTE) {
localObsoleteUris.add(uri);
}
}
}
if (localObsoleteUris.isNotEmpty) {
await removeEntries(localObsoleteUris, includeTrash: false);
}
obsoleteContentIds.forEach(changedUriByContentId.remove);
// fetch new entries
final tempUris = <String>{};
final newEntries = <AvesEntry>{}, entriesToRefresh = <AvesEntry>{};
final existingDirectories = <String>{};
for (final kv in changedUriByContentId.entries) {
final contentId = kv.key;
final uri = kv.value;
final sourceEntry = await mediaFetchService.getEntry(uri, null);
if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date`
if (existingEntry == null || (sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) || sourceEntry.path != existingEntry.path) {
if (existingEntry == null ||
(sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) ||
sourceEntry.path != existingEntry.path) {
final newPath = sourceEntry.path;
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
if (volume != null) {
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
} else if (_canAnalyze) {
// it can discover new entries only if it can analyze them
sourceEntry.id = localMediaDb.nextId;
newEntries.add(sourceEntry);
}
@ -340,7 +355,6 @@ class MediaStoreSource extends CollectionSource {
existingDirectories.add(existingDirectory);
}
} else {
debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...');
tempUris.add(uri);
}
}
@ -359,15 +373,12 @@ class MediaStoreSource extends CollectionSource {
if (newEntries.isNotEmpty) {
await localMediaDb.insertEntries(newEntries);
// TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => duplicateId == v.id);
newEntries.removeWhere((v) => v.id == duplicateId);
tempUris.add(duplicate.uri);
}
}
@ -380,8 +391,6 @@ class MediaStoreSource extends CollectionSource {
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
}
unawaited(reportService.log('$runtimeType refresh end for ${changedUris.length} uris'));
state = SourceState.ready;
return tempUris;
@ -415,8 +424,6 @@ class MediaStoreSource extends CollectionSource {
_lastGeneration = await mediaStoreService.getGeneration();
}
// vault
Future<void> _loadVaultEntries(String? directory) async {
addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
}

View file

@ -0,0 +1,451 @@
import 'dart:async';
import 'package:aves/model/covers.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
class MediaStoreSource extends CollectionSource {
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
final Set<String> _changedUris = {};
int? _lastGeneration;
SourceScope _loadedScope, _targetScope;
bool _canAnalyze = true;
Future<void>? _essentialLoader;
@override
set canAnalyze(bool enabled) => _canAnalyze = enabled;
@override
SourceScope get loadedScope => _loadedScope;
@override
SourceScope get targetScope => _targetScope;
@override
Future<void> init({
required SourceScope scope,
AnalysisController? analysisController,
bool loadTopEntriesFirst = false,
}) async {
_targetScope = scope;
await reportService.log('$runtimeType init target scope=$scope');
_essentialLoader ??= _loadEssentials();
await _essentialLoader;
addDirectories(albums: settings.pinnedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).toSet());
await updateGeneration();
unawaited(
_loadEntries(
analysisController: analysisController,
loadTopEntriesFirst: loadTopEntriesFirst,
),
);
}
Future<void> _loadEssentials() async {
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
await localMediaDb.init();
await vaults.init();
await favourites.init();
albumGrouping.init();
albumGrouping.setGroups(settings.albumGroups);
albumGrouping.registerSource(this);
tagGrouping.init();
tagGrouping.setGroups(settings.tagGroups);
tagGrouping.registerSource(this);
await covers.init();
await dynamicAlbums.init();
final deviceOffset = DateTime.now().timeZoneOffset.inMilliseconds;
final catalogOffset = settings.catalogTimeZoneOffsetMillis;
if (deviceOffset != catalogOffset) {
unawaited(reportService.log('Time zone offset change: $catalogOffset -> $deviceOffset. Clear catalog metadata to get correct date/times.'));
await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneOffsetMillis = deviceOffset;
}
await loadDates();
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
}
Future<void> _loadEntries({
AnalysisController? analysisController,
required bool loadTopEntriesFirst,
}) async {
unawaited(reportService.log('$runtimeType load (known) start'));
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
clearEntries();
final scopeAlbumFilters = _targetScope?.whereType<StoredAlbumFilter>();
final scopeDirectory = scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
final Set<AvesEntry> topEntries = {};
if (loadTopEntriesFirst) {
final topIds = settings.topEntryIds?.toSet();
if (topIds != null) {
debugPrint('$runtimeType load ${stopwatch.elapsed} load ${topIds.length} top entries');
topEntries.addAll(await localMediaDb.loadEntriesById(topIds));
addEntries(topEntries);
}
}
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
final knownEntries = await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
final isLargeCollection = knownEntries.length > 80000;
if (isLargeCollection && settings.isErrorReportingAllowed) {
// disable error reporting for large collections, to prevent noisy OOM reports
settings.isErrorReportingAllowed = false;
}
unawaited(reportService.setCustomKey('is_large_collection', isLargeCollection));
unawaited(reportService.log('$runtimeType found ${knownEntries.length} known entries'));
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
final knownDateByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) {
final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
}
final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet();
knownEntries.removeAll(removedEntries);
// show known entries
debugPrint('$runtimeType load ${stopwatch.elapsed} add known entries');
// add entries without notifying, so that the collection is not refreshed
// with items that may be hidden right away because of their metadata
addEntries(knownEntries, notify: false);
// but use album notification without waiting for cataloguing
// so that it is more reactive when picking an album in view mode
notifyAlbumsChanged();
await _loadVaultEntries(scopeDirectory);
debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata');
if (scopeDirectory != null) {
final ids = knownLiveEntries.map((entry) => entry.id).toSet();
await loadCatalogMetadata(ids: ids);
await loadAddresses(ids: ids);
} else {
await loadCatalogMetadata();
await loadAddresses();
// trash
await loadTrashDetails();
unawaited(
deleteExpiredTrash().then(
(deletedUris) {
if (deletedUris.isNotEmpty) {
debugPrint('evicted ${deletedUris.length} expired items from the trash');
removeEntries(deletedUris, includeTrash: true);
}
},
onError: (error) => debugPrint('failed to evict expired trash error=$error'),
),
);
}
updateDerivedFilters();
// clean up obsolete entries
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} remove obsolete entries');
await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet());
}
_loadedScope = _targetScope;
unawaited(reportService.log('$runtimeType load (known) done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${removedEntries.length} removed'));
if (_canAnalyze) {
// it can discover new entries only if it can analyze them
await _loadNewEntries(
analysisController: analysisController,
directory: scopeDirectory,
knownLiveEntries: knownLiveEntries,
knownDateByContentId: knownDateByContentId,
);
} else {
state = SourceState.ready;
}
}
Future<void> _loadNewEntries({
required AnalysisController? analysisController,
required String? directory,
required Set<AvesEntry> knownLiveEntries,
required Map<int?, int?> knownDateByContentId,
}) async {
unawaited(reportService.log('$runtimeType load (new) start'));
final stopwatch = Stopwatch()..start();
// items to add to the collection
final newEntries = <AvesEntry>{};
// recover untracked trash items
debugPrint('$runtimeType load ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
newEntries.addAll(await recoverUntrackedTrashItems());
}
// verify paths because some apps move files without updating their `last modified date`
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete paths');
final knownPathByContentId = Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet();
movedContentIds.forEach((contentId) {
// make obsolete by resetting its modified date
knownDateByContentId[contentId] = 0;
});
// fetch new & modified entries
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
final knownContentIds = knownDateByContentId.keys.toSet();
mediaStoreService
.getEntries(knownDateByContentId, directory: directory)
.listen(
(entry) {
// when discovering modified entry with known content ID,
// reuse known entry ID to overwrite it while preserving favourites, etc.
final contentId = entry.contentId;
final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null;
entry.id = existingEntry?.id ?? localMediaDb.nextId;
newEntries.add(entry);
setProgress(done: newEntries.length, total: 0);
},
onDone: () async {
if (newEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} save ${newEntries.length} new entries');
await localMediaDb.insertEntries(newEntries);
// TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => duplicateId == v.id);
}
}
// update trash details, if any
await Future.forEach(newEntries.where((v) => v.trashed), (entry) async {
final trashDetails = entry.trashDetails;
if (trashDetails != null) {
await localMediaDb.updateTrash(entry.id, trashDetails);
} else {
unawaited(reportService.recordError(Exception('Adding trashed entry but trash details are missing for entry=$entry')));
}
});
addEntries(newEntries);
// new entries include existing entries with obsolete paths
// so directories may be added, but also removed or simply have their content summary changed
invalidateAlbumFilterSummary();
updateDirectories();
}
debugPrint('$runtimeType load ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
// not only visible entries, as hidden and vault items may be analyzed
analysisEntries = allEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
}
await analyze(analysisController, entries: analysisEntries);
// the home page may not reflect the current derived filters
// as the initial addition of entries is silent,
// so we manually notify change for potential home screen filters
notifyAlbumsChanged();
unawaited(reportService.log('$runtimeType load (new) done in ${stopwatch.elapsed.inSeconds}s for ${newEntries.length} new entries'));
},
onError: (error) => debugPrint('$runtimeType stream error=$error'),
);
}
// returns URIs to retry later. They could be URIs that are:
// 1) currently being processed during bulk move/deletion
// 2) registered in the Media Store but still being processed by their owner in a temporary location
// For example, when taking a picture with a Galaxy S10e default camera app, querying the Media Store
// sometimes yields an entry with its temporary path: `/data/sec/camera/!@#$%^..._temp.jpg`
@override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (!canRefresh || _essentialLoader == null || !isReady) return changedUris;
state = SourceState.loading;
unawaited(reportService.log('$runtimeType refresh start for ${changedUris.length} uris'));
final changedUriByContentId = Map.fromEntries(
changedUris.map((uri) {
final pathSegments = Uri.parse(uri).pathSegments;
// e.g. URI `content://media/` has no path segment
if (pathSegments.isEmpty) return null;
final idString = pathSegments.last;
final contentId = int.tryParse(idString);
if (contentId == null) return null;
return MapEntry(contentId, uri);
}).nonNulls,
);
// clean up obsolete entries
final obsoleteContentIds = (await mediaStoreService.checkObsoleteContentIds(changedUriByContentId.keys.toList())).toSet();
final obsoleteUris = obsoleteContentIds.map((contentId) => changedUriByContentId[contentId]).nonNulls.toSet();
await removeEntries(obsoleteUris, includeTrash: false);
obsoleteContentIds.forEach(changedUriByContentId.remove);
// fetch new entries
final tempUris = <String>{};
final newEntries = <AvesEntry>{}, entriesToRefresh = <AvesEntry>{};
final existingDirectories = <String>{};
for (final kv in changedUriByContentId.entries) {
final contentId = kv.key;
final uri = kv.value;
final sourceEntry = await mediaFetchService.getEntry(uri, null);
if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
// compare paths because some apps move files without updating their `last modified date`
if (existingEntry == null || (sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) || sourceEntry.path != existingEntry.path) {
final newPath = sourceEntry.path;
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
if (volume != null) {
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
} else if (_canAnalyze) {
// it can discover new entries only if it can analyze them
sourceEntry.id = localMediaDb.nextId;
newEntries.add(sourceEntry);
}
final existingDirectory = existingEntry?.directory;
if (existingDirectory != null) {
existingDirectories.add(existingDirectory);
}
} else {
debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...');
tempUris.add(uri);
}
}
}
}
await _refreshVaultEntries(
changedUris: changedUris.where(vaults.isVaultEntryUri).toSet(),
newEntries: newEntries,
entriesToRefresh: entriesToRefresh,
existingDirectories: existingDirectories,
);
invalidateAlbumFilterSummary(directories: existingDirectories);
if (newEntries.isNotEmpty) {
await localMediaDb.insertEntries(newEntries);
// TODO TLAD find duplication cause
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}')));
// post-error cleanup
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => duplicateId == v.id);
tempUris.add(duplicate.uri);
}
}
addEntries(newEntries);
await analyze(analysisController, entries: newEntries);
}
if (entriesToRefresh.isNotEmpty) {
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
}
unawaited(reportService.log('$runtimeType refresh end for ${changedUris.length} uris'));
state = SourceState.ready;
return tempUris;
}
void onStoreChanged(String? uri) {
if (uri != null) _changedUris.add(uri);
if (_changedUris.isNotEmpty) {
_changeDebouncer(() async {
final todo = _changedUris.toSet();
_changedUris.clear();
final tempUris = await refreshUris(todo);
if (tempUris.isNotEmpty) {
_changedUris.addAll(tempUris);
onStoreChanged(null);
}
});
}
}
Future<void> checkForChanges() async {
final sinceGeneration = _lastGeneration;
if (sinceGeneration != null) {
_changedUris.addAll(await mediaStoreService.getChangedUris(sinceGeneration));
onStoreChanged(null);
}
await updateGeneration();
}
Future<void> updateGeneration() async {
_lastGeneration = await mediaStoreService.getGeneration();
}
// vault
Future<void> _loadVaultEntries(String? directory) async {
addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
}
Future<void> _refreshVaultEntries({
required Set<String> changedUris,
required Set<AvesEntry> newEntries,
required Set<AvesEntry> entriesToRefresh,
required Set<String> existingDirectories,
}) async {
for (final uri in changedUris) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
final existingDirectory = existingEntry.directory;
if (existingDirectory != null) {
existingDirectories.add(existingDirectory);
}
} else {
final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
if (sourceEntry != null) {
newEntries.add(
sourceEntry.copyWith(
id: localMediaDb.nextId,
origin: EntryOrigins.vault,
),
);
}
}
}
}
}

View file

@ -0,0 +1,462 @@
// lib/model/source/media_store_source.dart
import 'dart:async';
import 'package:aves/model/covers.dart';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/origins.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves_model/aves_model.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
// solo per i contatori (facoltativi)
import 'package:sqflite/sqflite.dart' show Sqflite;
class MediaStoreSource extends CollectionSource {
final Debouncer _changeDebouncer = Debouncer(delay: ADurations.mediaContentChangeDebounceDelay);
final Set<String> _changedUris = {};
int? _lastGeneration;
SourceScope _loadedScope, _targetScope;
bool _canAnalyze = true;
Future<void>? _essentialLoader;
@override
set canAnalyze(bool enabled) => _canAnalyze = enabled;
@override
SourceScope get loadedScope => _loadedScope;
@override
SourceScope get targetScope => _targetScope;
@override
Future<void> init({
required SourceScope scope,
AnalysisController? analysisController,
bool loadTopEntriesFirst = false,
}) async {
_targetScope = scope;
await reportService.log('$runtimeType init target scope=$scope');
_essentialLoader ??= _loadEssentials();
await _essentialLoader;
addDirectories(albums: settings.pinnedFilters.whereType<StoredAlbumFilter>().map((v) => v.album).toSet());
await updateGeneration();
unawaited(
_loadEntries(
analysisController: analysisController,
loadTopEntriesFirst: loadTopEntriesFirst,
),
);
}
Future<void> _loadEssentials() async {
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
await localMediaDb.init();
await vaults.init();
await favourites.init();
albumGrouping.init();
albumGrouping.setGroups(settings.albumGroups);
albumGrouping.registerSource(this);
tagGrouping.init();
tagGrouping.setGroups(settings.tagGroups);
tagGrouping.registerSource(this);
await covers.init();
await dynamicAlbums.init();
final deviceOffset = DateTime.now().timeZoneOffset.inMilliseconds;
final catalogOffset = settings.catalogTimeZoneOffsetMillis;
if (deviceOffset != catalogOffset) {
unawaited(reportService.log(
'Time zone offset change: $catalogOffset -> $deviceOffset. Clear catalog metadata to get correct date/times.'));
await localMediaDb.clearDates();
await localMediaDb.clearCatalogMetadata();
settings.catalogTimeZoneOffsetMillis = deviceOffset;
}
await loadDates();
debugPrint('$runtimeType load essentials complete in ${stopwatch.elapsed.inMilliseconds}ms');
}
Future<void> _loadEntries({
AnalysisController? analysisController,
required bool loadTopEntriesFirst,
}) async {
unawaited(reportService.log('$runtimeType load (known) start'));
final stopwatch = Stopwatch()..start();
state = SourceState.loading;
final scopeAlbumFilters = _targetScope?.whereType<StoredAlbumFilter>();
final scopeDirectory =
scopeAlbumFilters != null && scopeAlbumFilters.length == 1 ? scopeAlbumFilters.first.album : null;
// 🔒 Sentinella: conteggio remoti PRIMA (facoltativo)
final preRem = Sqflite.firstIntValue(
await localMediaDb.rawDb.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=1'),
) ??
0;
debugPrint('[cleanup][pre] remoti in DB = $preRem');
// 🔧 PATCH: non azzerare tutta la tabella; cancella SOLO i locali
final swClear = Stopwatch()..start();
final deletedLocal = await localMediaDb.rawDb
.rawDelete('DELETE FROM entry WHERE origin = ?', [EntryOrigins.mediaStoreContent]);
swClear.stop();
debugPrint('$runtimeType load ${swClear.elapsed} clear local entries deleted $deletedLocal rows');
// 🔒 Sentinella: conteggio remoti DOPO (Δ deve essere 0)
final postRem = Sqflite.firstIntValue(
await localMediaDb.rawDb.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=1'),
) ??
0;
debugPrint('[cleanup][post] remoti in DB = $postRem (Δ=${postRem - preRem})');
final Set<AvesEntry> topEntries = {};
if (loadTopEntriesFirst) {
final topIds = settings.topEntryIds?.toSet();
if (topIds != null) {
debugPrint('$runtimeType load ${stopwatch.elapsed} load ${topIds.length} top entries');
topEntries.addAll(await localMediaDb.loadEntriesById(topIds));
addEntries(topEntries);
}
}
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch known entries');
final knownEntries =
await localMediaDb.loadEntries(origin: EntryOrigins.mediaStoreContent, directory: scopeDirectory);
final knownLiveEntries = knownEntries.where((entry) => !entry.trashed).toSet();
final isLargeCollection = knownEntries.length > 80000;
if (isLargeCollection && settings.isErrorReportingAllowed) {
settings.isErrorReportingAllowed = false;
}
unawaited(reportService.setCustomKey('is_large_collection', isLargeCollection));
unawaited(reportService.log('$runtimeType found ${knownEntries.length} known entries'));
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete entries');
final knownDateByContentId =
Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedMillis)));
final knownContentIds = knownDateByContentId.keys.toList();
final removedContentIds = (await mediaStoreService.checkObsoleteContentIds(knownContentIds)).toSet();
if (topEntries.isNotEmpty) {
final removedTopEntries = topEntries.where((entry) => removedContentIds.contains(entry.contentId));
await removeEntries(removedTopEntries.map((entry) => entry.uri).toSet(), includeTrash: false);
}
final removedEntries = knownEntries.where((entry) => removedContentIds.contains(entry.contentId)).toSet();
knownEntries.removeAll(removedEntries);
// show known entries
debugPrint('$runtimeType load ${stopwatch.elapsed} add known entries');
addEntries(knownEntries, notify: false);
notifyAlbumsChanged();
await _loadVaultEntries(scopeDirectory);
debugPrint('$runtimeType load ${stopwatch.elapsed} load metadata');
if (scopeDirectory != null) {
final ids = knownLiveEntries.map((entry) => entry.id).toSet();
await loadCatalogMetadata(ids: ids);
await loadAddresses(ids: ids);
} else {
await loadCatalogMetadata();
await loadAddresses();
// trash
await loadTrashDetails();
unawaited(
deleteExpiredTrash().then(
(deletedUris) {
if (deletedUris.isNotEmpty) {
debugPrint('evicted ${deletedUris.length} expired items from the trash');
removeEntries(deletedUris, includeTrash: true);
}
},
onError: (error) => debugPrint('failed to evict expired trash error=$error'),
),
);
}
updateDerivedFilters();
// clean up obsolete entries (sono locali: derivano dai knownEntries locali)
if (removedEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} remove obsolete entries');
await localMediaDb.removeIds(removedEntries.map((entry) => entry.id).toSet());
}
_loadedScope = _targetScope;
unawaited(reportService
.log('$runtimeType load (known) done in ${stopwatch.elapsed.inSeconds}s for ${knownEntries.length} known, ${removedEntries.length} removed'));
if (_canAnalyze) {
await _loadNewEntries(
analysisController: analysisController,
directory: scopeDirectory,
knownLiveEntries: knownLiveEntries,
knownDateByContentId: knownDateByContentId,
);
} else {
state = SourceState.ready;
}
}
Future<void> _loadNewEntries({
required AnalysisController? analysisController,
required String? directory,
required Set<AvesEntry> knownLiveEntries,
required Map<int?, int?> knownDateByContentId,
}) async {
unawaited(reportService.log('$runtimeType load (new) start'));
final stopwatch = Stopwatch()..start();
final newEntries = <AvesEntry>{};
// recover untracked trash items
debugPrint('$runtimeType load ${stopwatch.elapsed} recover untracked entries');
if (directory == null) {
newEntries.addAll(await recoverUntrackedTrashItems());
}
// verify paths
debugPrint('$runtimeType load ${stopwatch.elapsed} check obsolete paths');
final knownPathByContentId =
Map.fromEntries(knownLiveEntries.map((entry) => MapEntry(entry.contentId, entry.path)));
final movedContentIds = (await mediaStoreService.checkObsoletePaths(knownPathByContentId)).toSet();
movedContentIds.forEach((contentId) {
// mark obsolete (sulla mappa dei locali)
knownDateByContentId[contentId] = 0;
});
// fetch new & modified entries
debugPrint('$runtimeType load ${stopwatch.elapsed} fetch new entries');
final knownContentIds = knownDateByContentId.keys.toSet();
mediaStoreService.getEntries(knownDateByContentId, directory: directory).listen(
(entry) {
final contentId = entry.contentId;
final existingEntry = knownContentIds.contains(contentId)
? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId)
: null;
entry.id = existingEntry?.id ?? localMediaDb.nextId;
newEntries.add(entry);
setProgress(done: newEntries.length, total: 0);
},
onDone: () async {
if (newEntries.isNotEmpty) {
debugPrint('$runtimeType load ${stopwatch.elapsed} save ${newEntries.length} new entries');
await localMediaDb.insertEntries(newEntries);
// dedup locali
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService
.recordError(Exception('Loading entries yielded duplicates=${duplicates.join(', ')}')));
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => v.id == duplicateId);
}
}
// update trash details, if any
await Future.forEach(newEntries.where((v) => v.trashed), (entry) async {
final trashDetails = entry.trashDetails;
if (trashDetails != null) {
await localMediaDb.updateTrash(entry.id, trashDetails);
} else {
unawaited(reportService
.recordError(Exception('Adding trashed entry but trash details are missing for entry=$entry')));
}
});
addEntries(newEntries);
invalidateAlbumFilterSummary();
updateDirectories();
}
debugPrint('$runtimeType load ${stopwatch.elapsed} analyze');
Set<AvesEntry>? analysisEntries;
final analysisIds = analysisController?.entryIds;
if (analysisIds != null) {
analysisEntries = allEntries.where((entry) => analysisIds.contains(entry.id)).toSet();
}
await analyze(analysisController, entries: analysisEntries);
notifyAlbumsChanged();
unawaited(reportService
.log('$runtimeType load (new) done in ${stopwatch.elapsed.inSeconds}s for ${newEntries.length} new entries'));
},
onError: (error) => debugPrint('$runtimeType stream error=$error'),
);
}
@override
Future<Set<String>> refreshUris(Set<String> changedUris, {AnalysisController? analysisController}) async {
if (!canRefresh || _essentialLoader == null || !isReady) return changedUris;
state = SourceState.loading;
unawaited(reportService.log('$runtimeType refresh start for ${changedUris.length} uris'));
final changedUriByContentId = Map.fromEntries(
changedUris.map((uri) {
final pathSegments = Uri.parse(uri).pathSegments;
if (pathSegments.isEmpty) return null;
final idString = pathSegments.last;
final contentId = int.tryParse(idString);
if (contentId == null) return null;
return MapEntry(contentId, uri);
}).nonNulls,
);
// clean up obsolete entries (URIs di MediaStore => locali)
final obsoleteContentIds =
(await mediaStoreService.checkObsoleteContentIds(changedUriByContentId.keys.toList())).toSet();
final obsoleteUris =
obsoleteContentIds.map((contentId) => changedUriByContentId[contentId]).nonNulls.toSet();
await removeEntries(obsoleteUris, includeTrash: false);
obsoleteContentIds.forEach(changedUriByContentId.remove);
// fetch new entries
final tempUris = <String>{};
final newEntries = <AvesEntry>{}, entriesToRefresh = <AvesEntry>{};
final existingDirectories = <String>{};
for (final kv in changedUriByContentId.entries) {
final contentId = kv.key;
final uri = kv.value;
final sourceEntry = await mediaFetchService.getEntry(uri, null);
if (sourceEntry != null) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.contentId == contentId);
if (existingEntry == null ||
(sourceEntry.dateModifiedMillis ?? 0) > (existingEntry.dateModifiedMillis ?? 0) ||
sourceEntry.path != existingEntry.path) {
final newPath = sourceEntry.path;
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
if (volume != null) {
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
} else if (_canAnalyze) {
sourceEntry.id = localMediaDb.nextId;
newEntries.add(sourceEntry);
}
final existingDirectory = existingEntry?.directory;
if (existingDirectory != null) {
existingDirectories.add(existingDirectory);
}
} else {
debugPrint('$runtimeType refreshUris entry=$sourceEntry is not located on a known storage volume. Will retry soon...');
tempUris.add(uri);
}
}
}
}
await _refreshVaultEntries(
changedUris: changedUris.where(vaults.isVaultEntryUri).toSet(),
newEntries: newEntries,
entriesToRefresh: entriesToRefresh,
existingDirectories: existingDirectories,
);
invalidateAlbumFilterSummary(directories: existingDirectories);
if (newEntries.isNotEmpty) {
await localMediaDb.insertEntries(newEntries);
final duplicates = await localMediaDb.searchLiveDuplicates(EntryOrigins.mediaStoreContent, newEntries);
if (duplicates.isNotEmpty) {
unawaited(reportService
.recordError(Exception('Refreshing entries yielded duplicates=${duplicates.join(', ')}')));
await localMediaDb.removeIds(duplicates.map((v) => v.id).toSet());
for (final duplicate in duplicates) {
final duplicateId = duplicate.id;
newEntries.removeWhere((v) => v.id == duplicateId);
tempUris.add(duplicate.uri);
}
}
addEntries(newEntries);
await analyze(analysisController, entries: newEntries);
}
if (entriesToRefresh.isNotEmpty) {
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
}
unawaited(reportService.log('$runtimeType refresh end for ${changedUris.length} uris'));
state = SourceState.ready;
return tempUris;
}
void onStoreChanged(String? uri) {
if (uri != null) _changedUris.add(uri);
if (_changedUris.isNotEmpty) {
_changeDebouncer(() async {
final todo = _changedUris.toSet();
_changedUris.clear();
final tempUris = await refreshUris(todo);
if (tempUris.isNotEmpty) {
_changedUris.addAll(tempUris);
onStoreChanged(null);
}
});
}
}
Future<void> checkForChanges() async {
final sinceGeneration = _lastGeneration;
if (sinceGeneration != null) {
_changedUris.addAll(await mediaStoreService.getChangedUris(sinceGeneration));
onStoreChanged(null);
}
await updateGeneration();
}
Future<void> updateGeneration() async {
_lastGeneration = await mediaStoreService.getGeneration();
}
// vault
Future<void> _loadVaultEntries(String? directory) async {
addEntries(await localMediaDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
}
Future<void> _refreshVaultEntries({
required Set<String> changedUris,
required Set<AvesEntry> newEntries,
required Set<AvesEntry> entriesToRefresh,
required Set<String> existingDirectories,
}) async {
for (final uri in changedUris) {
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
if (existingEntry != null) {
entriesToRefresh.add(existingEntry);
final existingDirectory = existingEntry.directory;
if (existingDirectory != null) {
existingDirectories.add(existingDirectory);
}
} else {
final sourceEntry = await mediaFetchService.getEntry(uri, null, allowUnsized: true);
if (sourceEntry != null) {
newEntries.add(
sourceEntry.copyWith(
id: localMediaDb.nextId,
origin: EntryOrigins.vault,
),
);
}
}
}
}
}

View file

@ -0,0 +1,25 @@
// lib/remote/collection_source_remote_ext.dart
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/common/services.dart';
import 'package:flutter/foundation.dart';
extension CollectionSourceRemoteExt on CollectionSource {
/// Carica dal DB tutte le entry remote (origin=1) non cestinate
/// e le aggiunge alla CollectionSource, con log di diagnostica.
Future<void> appendRemoteEntriesFromDb() async {
// 1) carica dal DB
final remoti = await localMediaDb.loadEntries(origin: 1);
debugPrint('[remote-append] candidati=${remoti.length}');
// 2) filtra visibili (!!! booleano, NON e.trashed == 0)
final visibili = remoti.where((e) => !e.trashed).toSet();
debugPrint('[remote-append] visibili=${visibili.length}');
// 3) aggiungi alla source (usa allEntries, non "entries")
final prima = allEntries.where((e) => e.origin == 1 && !e.trashed).length;
addEntries(visibili);
final dopo = allEntries.where((e) => e.origin == 1 && !e.trashed).length;
debugPrint('[remote-append] appese=${dopo - prima} (prima=$prima -> dopo=$dopo)');
}
}

View file

@ -1,96 +0,0 @@
// lib/remote/auth_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Gestisce autenticazione remota e caching del Bearer token.
/// - [baseUrl]: URL base del server (con o senza '/')
/// - [email]/[password]: credenziali
/// - [loginPath]: path dell'endpoint di login (default 'auth/login')
/// - [timeout]: timeout per le richieste (default 20s)
class RemoteAuth {
final Uri base;
final String email;
final String password;
final String loginPath;
final Duration timeout;
String? _token;
RemoteAuth({
required String baseUrl,
required this.email,
required this.password,
this.loginPath = 'auth/login',
this.timeout = const Duration(seconds: 20),
}) : base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
Uri get _loginUri => base.resolve(loginPath);
/// Esegue il login e memorizza il token.
/// Lancia eccezione con messaggio chiaro in caso di errore HTTP, rete o JSON.
Future<String> login() async {
final uri = _loginUri;
final headers = {'Content-Type': 'application/json'};
final bodyStr = json.encode({'email': email, 'password': password});
http.Response res;
try {
res = await http
.post(uri, headers: headers, body: bodyStr)
.timeout(timeout);
} catch (e) {
throw Exception('Login fallito: errore di rete verso $uri: $e');
}
// Follow esplicito per redirect POST moderni (307/308) mantenendo metodo e body
if ({307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
final redirectUri = uri.resolve(res.headers['location']!);
try {
res = await http
.post(redirectUri, headers: headers, body: bodyStr)
.timeout(timeout);
} catch (e) {
throw Exception('Login fallito: errore di rete verso $redirectUri: $e');
}
}
if (res.statusCode != 200) {
final snippet = utf8.decode(res.bodyBytes.take(200).toList());
throw Exception(
'Login fallito: HTTP ${res.statusCode} ${res.reasonPhrase} $snippet',
);
}
// Parsing JSON robusto
Map<String, dynamic> map;
try {
map = json.decode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
} catch (_) {
throw Exception('Login fallito: risposta non è un JSON valido');
}
// Supporto sia 'token' sia 'access_token'
final token = (map['token'] ?? map['access_token']) as String?;
if (token == null || token.isEmpty) {
throw Exception('Login fallito: token assente nella risposta');
}
_token = token;
return token;
}
/// Ritorna gli header con Bearer; se non hai token, esegue login.
Future<Map<String, String>> authHeaders() async {
_token ??= await login();
return {'Authorization': 'Bearer $_token'};
}
/// Forza il rinnovo del token (es. dopo 401) e ritorna i nuovi header.
Future<Map<String, String>> refreshAndHeaders() async {
_token = null;
return await authHeaders();
}
/// Accesso in sola lettura al token corrente (può essere null).
String? get token => _token;
}

View file

@ -1,93 +0,0 @@
// lib/remote/remote_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'remote_models.dart';
import 'auth_client.dart';
class RemoteJsonClient {
final Uri indexUri; // es. https://prova.patachina.it/photos/
final RemoteAuth? auth; // opzionale: se presente, aggiunge Bearer
RemoteJsonClient(
String baseUrl,
String indexPath, {
this.auth,
}) : indexUri = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/')
.resolve(indexPath);
Future<List<RemotePhotoItem>> fetchAll() async {
Map<String, String> headers = {};
if (auth != null) {
headers = await auth!.authHeaders();
}
// DEBUG: stampa la URL precisa
// ignore: avoid_print
print('[remote-client] GET $indexUri');
http.Response res;
try {
res = await http.get(indexUri, headers: headers).timeout(const Duration(seconds: 20));
} catch (e) {
throw Exception('Errore rete su $indexUri: $e');
}
// Retry 1 volta in caso di 401 (token scaduto/invalidato)
if (res.statusCode == 401 && auth != null) {
headers = await auth!.refreshAndHeaders();
res = await http.get(indexUri, headers: headers).timeout(const Duration(seconds: 20));
}
// Follow 30x mantenendo Authorization
if ({301, 302, 307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
final loc = res.headers['location']!;
final redirectUri = indexUri.resolve(loc);
res = await http.get(redirectUri, headers: headers).timeout(const Duration(seconds: 20));
}
if (res.statusCode != 200) {
final snippet = utf8.decode(res.bodyBytes.take(200).toList());
throw Exception('HTTP ${res.statusCode} ${res.reasonPhrase} su $indexUri. Body: $snippet');
}
final body = utf8.decode(res.bodyBytes);
// Qui siamo espliciti: ci aspettiamo SEMPRE una lista top-level
final dynamic decoded = json.decode(body);
if (decoded is! List) {
throw Exception('JSON inatteso: atteso array top-level, ricevuto ${decoded.runtimeType}');
}
final List<dynamic> rawList = decoded;
// --- DIAGNOSTICA: conteggio pattern dai dati del SERVER (non stampo il JSON intero)
int withOriginal = 0, withoutOriginal = 0, leadingSlash = 0, noLeadingSlash = 0;
for (final e in rawList) {
if (e is Map<String, dynamic>) {
final p = (e['path'] ?? '').toString();
if (p.startsWith('/')) {
leadingSlash++;
} else {
noLeadingSlash++;
}
if (p.contains('/original/')) {
withOriginal++;
} else {
withoutOriginal++;
}
}
}
// ignore: avoid_print
print('[remote-client] SERVER paths -> withOriginal=$withOriginal | withoutOriginal=$withoutOriginal | '
'leadingSlash=$leadingSlash | noLeadingSlash=$noLeadingSlash');
// Costruiamo List<RemotePhotoItem>
final List<RemotePhotoItem> items = rawList.map<RemotePhotoItem>((e) {
if (e is! Map<String, dynamic>) {
throw Exception('Elemento JSON non è una mappa: ${e.runtimeType} -> $e');
}
return RemotePhotoItem.fromJson(e);
}).toList();
return items;
}
}

View file

@ -1,125 +0,0 @@
// lib/remote/remote_models.dart
import 'url_utils.dart';
class RemotePhotoItem {
final String id;
final String name;
final String path;
final String? thub1, thub2;
final String? mimeType;
final int? width, height, sizeBytes;
final DateTime? takenAtUtc;
final double? lat, lng, alt;
final String? dataExifLegacy;
final String? user;
final int? durationMillis;
final RemoteLocation? location;
RemotePhotoItem({
required this.id,
required this.name,
required this.path,
this.thub1,
this.thub2,
this.mimeType,
this.width,
this.height,
this.sizeBytes,
this.takenAtUtc,
this.lat,
this.lng,
this.alt,
this.dataExifLegacy,
this.user,
this.durationMillis,
this.location,
});
// URL completo costruito solo in fase di lettura
// String get uri => "https://prova.patachina.it/$path";
// Costruzione URL assoluto delegata a utility (in base alle impostazioni)
String absoluteUrl(String baseUrl) => buildAbsoluteUri(baseUrl, path).toString();
static DateTime? _tryParseIsoUtc(dynamic v) {
if (v == null) return null;
try { return DateTime.parse(v.toString()).toUtc(); } catch (_) { return null; }
}
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
static int? _toMillis(dynamic v) {
if (v == null) return null;
final num? n = (v is num) ? v : num.tryParse(v.toString());
if (n == null) return null;
return n >= 1000 ? n.toInt() : (n * 1000).toInt();
}
factory RemotePhotoItem.fromJson(Map<String, dynamic> j) {
final gps = j['gps'] as Map<String, dynamic>?;
final loc = j['location'] is Map<String, dynamic>
? RemoteLocation.fromJson(j['location'] as Map<String, dynamic>)
: null;
return RemotePhotoItem(
id: (j['id'] ?? j['name']).toString(),
name: (j['name'] ?? '').toString(),
path: (j['path'] ?? '').toString(),
thub1: j['thub1']?.toString(),
thub2: j['thub2']?.toString(),
mimeType: j['mime_type']?.toString(),
width: (j['width'] as num?)?.toInt(),
height: (j['height'] as num?)?.toInt(),
sizeBytes: (j['size_bytes'] as num?)?.toInt(),
takenAtUtc: _tryParseIsoUtc(j['taken_at']),
dataExifLegacy: j['data']?.toString(),
lat: gps != null ? _toDouble(gps['lat']) : null,
lng: gps != null ? _toDouble(gps['lng']) : null,
alt: gps != null ? _toDouble(gps['alt']) : null,
user: j['user']?.toString(),
durationMillis: _toMillis(j['duration']),
location: loc,
);
}
}
class RemoteLocation {
final String? continent;
final String? country;
final String? region;
final String? postcode;
final String? city;
final String? countyCode;
final String? address;
final String? timezone;
final String? timeOffset;
RemoteLocation({
this.continent,
this.country,
this.region,
this.postcode,
this.city,
this.countyCode,
this.address,
this.timezone,
this.timeOffset,
});
factory RemoteLocation.fromJson(Map<String, dynamic> j) => RemoteLocation(
continent: j['continent']?.toString(),
country: j['country']?.toString(),
region: j['region']?.toString(),
postcode: j['postcode']?.toString(),
city: j['city']?.toString(),
countyCode:j['county_code']?.toString(),
address: j['address']?.toString(),
timezone: j['timezone']?.toString(),
timeOffset:j['time']?.toString(),
);
}

View file

@ -1,375 +0,0 @@
// lib/remote/remote_repository.dart
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:sqflite/sqflite.dart';
import 'remote_models.dart';
class RemoteRepository {
final Database db;
RemoteRepository(this.db);
// =========================
// Helpers PRAGMA / schema
// =========================
Future<void> _ensureColumns(
DatabaseExecutor dbExec, {
required String table,
required Map<String, String> columnsAndTypes,
}) async {
try {
final rows = await dbExec.rawQuery('PRAGMA table_info($table);');
final existing = rows.map((r) => (r['name'] as String)).toSet();
for (final entry in columnsAndTypes.entries) {
final col = entry.key;
final typ = entry.value;
if (!existing.contains(col)) {
final sql = 'ALTER TABLE $table ADD COLUMN $col $typ;';
try {
await dbExec.execute(sql);
debugPrint('[RemoteRepository] executed: $sql');
} catch (e, st) {
debugPrint('[RemoteRepository] failed to execute $sql: $e\n$st');
}
}
}
} catch (e, st) {
debugPrint('[RemoteRepository] _ensureColumns($table) error: $e\n$st');
}
}
/// Assicura che le colonne GPS e alcune colonne "remote*" esistano nella tabella `entry`.
Future<void> _ensureEntryColumns(DatabaseExecutor dbExec) async {
await _ensureColumns(dbExec, table: 'entry', columnsAndTypes: const {
// GPS
'latitude': 'REAL',
'longitude': 'REAL',
'altitude': 'REAL',
// Campi remoti
'remoteId': 'TEXT',
'remotePath': 'TEXT',
'remoteThumb1': 'TEXT',
'remoteThumb2': 'TEXT',
'origin': 'INTEGER',
'provider': 'TEXT',
'trashed': 'INTEGER',
});
// Indice "normale" per velocizzare il lookup su remoteId
try {
await dbExec.execute(
'CREATE INDEX IF NOT EXISTS idx_entry_remoteId ON entry(remoteId);',
);
} catch (e, st) {
debugPrint('[RemoteRepository] create index error: $e\n$st');
}
}
// =========================
// Retry su SQLITE_BUSY
// =========================
bool _isBusy(Object e) {
final s = e.toString();
return s.contains('SQLITE_BUSY') || s.contains('database is locked');
}
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
if (!_isBusy(e) || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
// =========================
// Normalizzazione SOLO per diagnostica (non cambia cosa salvi)
// =========================
String _normPath(String? p) {
if (p == null || p.isEmpty) return '';
var s = p.trim().replaceAll(RegExp(r'/+'), '/');
if (!s.startsWith('/')) s = '/$s';
return s;
}
/// Candidato "canonico" (inserisce '/original/' dopo '/photos/<User>/'
/// se manca). Usato solo per LOG/HINT, NON per scrivere.
String _canonCandidate(String? rawPath, String fileName) {
var s = _normPath(rawPath);
final seg = s.split('/'); // ['', 'photos', '<User>', maybe 'original', ...]
if (seg.length >= 4 && seg[1] == 'photos' && seg[3] != 'original' && seg[3] != 'thumbs') {
seg.insert(3, 'original');
}
if (fileName.isNotEmpty) {
seg[seg.length - 1] = fileName;
}
return seg.join('/');
}
// =========================
// Utilities
// =========================
bool _isVideoItem(RemotePhotoItem it) {
final mt = (it.mimeType ?? '').toLowerCase();
final p = (it.path).toLowerCase();
return mt.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Map<String, Object?> _buildEntryRow(RemotePhotoItem it, {int? existingId}) {
// NON correggo: salvo esattamente quello che arriva (come ora)
return <String, Object?>{
'id': existingId,
'contentId': null,
'uri': null,
'path': it.path,
'sourceMimeType': it.mimeType,
'width': it.width,
'height': it.height,
'sourceRotationDegrees': null,
'sizeBytes': it.sizeBytes,
'title': it.name,
'dateAddedSecs': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'dateModifiedMillis': null,
'sourceDateTakenMillis': it.takenAtUtc?.millisecondsSinceEpoch,
'durationMillis': it.durationMillis,
// REMOTI VISIBILI (come nel tuo file attuale)
'trashed': 0,
'origin': 1,
'provider': 'json@patachina',
// GPS (possono essere null)
'latitude': it.lat,
'longitude': it.lng,
'altitude': it.alt,
// campi remoti ( path raw, senza forzare /original/)
'remoteId': it.id,
'remotePath': it.path,
'remoteThumb1': it.thub1,
'remoteThumb2': it.thub2,
};
}
Map<String, Object?> _buildAddressRow(int newId, RemoteLocation location) {
return <String, Object?>{
'id': newId,
'addressLine': location.address,
'countryCode': null,
'countryName': location.country,
'adminArea': location.region,
'locality': location.city,
};
}
// =========================
// Upsert a chunk (DIAGNOSTICA inclusa)
// =========================
Future<void> upsertAll(List<RemotePhotoItem> items, {int chunkSize = 200}) async {
debugPrint('RemoteRepository.upsertAll: items=${items.length}');
if (items.isEmpty) return;
await _withRetryBusy(() => _ensureEntryColumns(db));
// Ordina: prima immagini, poi video
final images = <RemotePhotoItem>[];
final videos = <RemotePhotoItem>[];
for (final it in items) {
(_isVideoItem(it) ? videos : images).add(it);
}
final ordered = <RemotePhotoItem>[...images, ...videos];
for (var offset = 0; offset < ordered.length; offset += chunkSize) {
final end = (offset + chunkSize < ordered.length) ? offset + chunkSize : ordered.length;
final chunk = ordered.sublist(offset, end);
try {
await _withRetryBusy(() => db.transaction((txn) async {
final batch = txn.batch();
for (final it in chunk) {
// === DIAGNOSTICA PRE-LOOKUP ===
final raw = it.path;
final norm = _normPath(raw);
final cand = _canonCandidate(raw, it.name);
final hasOriginal = raw.contains('/original/');
final hasLeading = raw.startsWith('/');
debugPrint(
'[repo-upsert] in: rid=${it.id.substring(0,8)} name=${it.name} '
'raw="$raw" (original=${hasOriginal?"Y":"N"}, leading=${hasLeading?"Y":"N"})'
);
// Lookup record esistente SOLO per remoteId (comportamento attuale)
int? existingId;
try {
final existing = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remoteId = ?',
whereArgs: [it.id],
limit: 1,
);
existingId = existing.isNotEmpty ? (existing.first['id'] as int?) : null;
} catch (e, st) {
debugPrint('[RemoteRepository] lookup existingId failed for remoteId=${it.id}: $e\n$st');
}
// === DIAGNOSTICA HINT: esisterebbe una riga compatibile per path? ===
// 1) path canonico (con /original/)
try {
final byCanon = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [cand],
limit: 1,
);
if (byCanon.isNotEmpty && existingId == null) {
final idCand = byCanon.first['id'];
debugPrint(
'[repo-upsert][HINT] trovata riga per CAND-remotePath="$cand" -> id=$idCand '
'(il lookup corrente per remoteId NON la vede: possibile causa duplicato)'
);
}
} catch (_) {}
// 2) path raw normalizzato (solo slash)
try {
final byNorm = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [norm],
limit: 1,
);
if (byNorm.isNotEmpty && existingId == null) {
final idNorm = byNorm.first['id'];
debugPrint(
'[repo-upsert][HINT] trovata riga per RAW-NORM-remotePath="$norm" -> id=$idNorm '
'(il lookup corrente per remoteId NON la vede: possibile causa duplicato)'
);
}
} catch (_) {}
// Riga completa ( salviamo il RAW come stai facendo ora)
final row = _buildEntryRow(it, existingId: existingId);
// Insert/replace
try {
batch.insert(
'entry',
row,
conflictAlgorithm: ConflictAlgorithm.replace,
);
} on DatabaseException catch (e, st) {
debugPrint('[RemoteRepository] batch insert failed for remoteId=${it.id}: $e\n$st');
final rowNoGps = Map<String, Object?>.from(row)
..remove('latitude')
..remove('longitude')
..remove('altitude');
batch.insert(
'entry',
rowNoGps,
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
await batch.commit(noResult: true);
// Secondo pass per address (immutato)
for (final it in chunk) {
if (it.location == null) continue;
try {
final rows = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remoteId = ?',
whereArgs: [it.id],
limit: 1,
);
if (rows.isEmpty) continue;
final newId = rows.first['id'] as int;
final addr = _buildAddressRow(newId, it.location!);
await txn.insert(
'address',
addr,
conflictAlgorithm: ConflictAlgorithm.replace,
);
} catch (e, st) {
debugPrint('[RemoteRepository] insert address failed for remoteId=${it.id}: $e\n$st');
}
}
}));
} catch (e, st) {
debugPrint('[RemoteRepository] upsert chunk ${offset}..${end - 1} ERROR: $e\n$st');
rethrow;
}
}
}
// =========================
// Unicità & deduplica (immutato)
// =========================
Future<void> ensureUniqueRemoteId() async {
try {
await db.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_entry_remote_remoteId '
'ON entry(remoteId) WHERE origin=1',
);
debugPrint('[RemoteRepository] ensured UNIQUE index on entry(remoteId) for origin=1');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureUniqueRemoteId error: $e\n$st');
}
}
Future<int> deduplicateRemotes() async {
try {
final deleted = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remoteId IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remoteId IS NOT NULL '
' GROUP BY remoteId'
')',
);
debugPrint('[RemoteRepository] deduplicateRemotes deleted=$deleted');
return deleted;
} catch (e, st) {
debugPrint('[RemoteRepository] deduplicateRemotes error: $e\n$st');
return 0;
}
}
Future<void> sanitizeRemotes() async {
await deduplicateRemotes();
await ensureUniqueRemoteId();
}
// =========================
// Utils
// =========================
Future<int> countRemote() async {
final rows = await db.rawQuery('SELECT COUNT(1) AS c FROM entry WHERE origin=1');
return (rows.first['c'] as int?) ?? 0;
}
}

View file

@ -1,83 +0,0 @@
// lib/remote/remote_settings.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class RemoteSettings {
static const _storage = FlutterSecureStorage();
// Keys
static const _kEnabled = 'remote_enabled';
static const _kBaseUrl = 'remote_base_url';
static const _kIndexPath = 'remote_index_path';
static const _kEmail = 'remote_email';
static const _kPassword = 'remote_password';
// Default values:
// In DEBUG vogliamo valori pre-compilati; in RELEASE lasciamo vuoti/false.
static final bool defaultEnabled = kDebugMode ? true : false;
static final String defaultBaseUrl = kDebugMode ? 'https://prova.patachina.it' : '';
static final String defaultIndexPath = kDebugMode ? 'photos/' : '';
static final String defaultEmail = kDebugMode ? 'fabio@gmail.com' : '';
static final String defaultPassword = kDebugMode ? 'master66' : '';
bool enabled;
String baseUrl;
String indexPath;
String email;
String password;
RemoteSettings({
required this.enabled,
required this.baseUrl,
required this.indexPath,
required this.email,
required this.password,
});
/// Carica i setting dal secure storage.
/// Se un valore non esiste, usa i default (in debug: quelli precompilati).
static Future<RemoteSettings> load() async {
final enabledStr = await _storage.read(key: _kEnabled);
final baseUrl = await _storage.read(key: _kBaseUrl) ?? defaultBaseUrl;
final indexPath = await _storage.read(key: _kIndexPath) ?? defaultIndexPath;
final email = await _storage.read(key: _kEmail) ?? defaultEmail;
final password = await _storage.read(key: _kPassword) ?? defaultPassword;
final enabled = (enabledStr ?? (defaultEnabled ? 'true' : 'false')) == 'true';
return RemoteSettings(
enabled: enabled,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
}
/// Scrive i setting nel secure storage.
Future<void> save() async {
await _storage.write(key: _kEnabled, value: enabled ? 'true' : 'false');
await _storage.write(key: _kBaseUrl, value: baseUrl);
await _storage.write(key: _kIndexPath, value: indexPath);
await _storage.write(key: _kEmail, value: email);
await _storage.write(key: _kPassword, value: password);
}
/// In DEBUG: se un valore non è ancora impostato, inizializzalo con i default.
/// NON sovrascrive valori già presenti (quindi puoi sempre entrare in Settings e cambiare).
static Future<void> debugSeedIfEmpty() async {
if (!kDebugMode) return;
Future<void> _seed(String key, String value) async {
final existing = await _storage.read(key: key);
if (existing == null) {
await _storage.write(key: key, value: value);
}
}
await _seed(_kEnabled, defaultEnabled ? 'true' : 'false');
await _seed(_kBaseUrl, defaultBaseUrl);
await _seed(_kIndexPath, defaultIndexPath);
await _seed(_kEmail, defaultEmail);
await _seed(_kPassword, defaultPassword);
}
}

View file

@ -1,94 +0,0 @@
import 'package:flutter/material.dart';
import 'remote_settings.dart';
class RemoteSettingsPage extends StatefulWidget {
const RemoteSettingsPage({super.key});
@override
State<RemoteSettingsPage> createState() => _RemoteSettingsPageState();
}
class _RemoteSettingsPageState extends State<RemoteSettingsPage> {
final _form = GlobalKey<FormState>();
bool _enabled = RemoteSettings.defaultEnabled;
final _baseUrl = TextEditingController(text: RemoteSettings.defaultBaseUrl);
final _indexPath = TextEditingController(text: RemoteSettings.defaultIndexPath);
final _email = TextEditingController();
final _password = TextEditingController();
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final s = await RemoteSettings.load();
setState(() {
_enabled = s.enabled;
_baseUrl.text = s.baseUrl;
_indexPath.text = s.indexPath;
_email.text = s.email;
_password.text = s.password;
});
}
Future<void> _save() async {
if (!_form.currentState!.validate()) return;
final s = RemoteSettings(
enabled: _enabled,
baseUrl: _baseUrl.text.trim(),
indexPath: _indexPath.text.trim(),
email: _email.text.trim(),
password: _password.text,
);
await s.save();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Impostazioni remote salvate')));
Navigator.of(context).maybePop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Remote Settings')),
body: Form(
key: _form,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Abilita sync remoto'),
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
),
TextFormField(
controller: _baseUrl,
decoration: const InputDecoration(labelText: 'Base URL (es. https://server.tld)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _indexPath,
decoration: const InputDecoration(labelText: 'Index path (es. photos/)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _email,
decoration: const InputDecoration(labelText: 'User/Email'),
),
TextFormField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: const Text('Salva'),
),
],
),
),
);
}
}

View file

@ -1,647 +0,0 @@
// lib/remote/remote_test_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sqflite/sqflite.dart';
// Integrazione impostazioni & auth remota (Fase 1)
import 'remote_settings.dart';
import 'auth_client.dart';
import 'url_utils.dart';
enum _RemoteFilter { all, visibleOnly, trashedOnly }
class RemoteTestPage extends StatefulWidget {
final Database db;
/// Base URL preferita (es. https://prova.patachina.it).
/// Se non la passi o è vuota, verrà usata quella in RemoteSettings.
final String? baseUrl;
const RemoteTestPage({
super.key,
required this.db,
this.baseUrl,
});
@override
State<RemoteTestPage> createState() => _RemoteTestPageState();
}
class _RemoteTestPageState extends State<RemoteTestPage> {
Future<List<_RemoteRow>>? _future;
String _baseUrl = '';
Map<String, String>? _authHeaders;
bool _navigating = false; // debounce del tap
_RemoteFilter _filter = _RemoteFilter.all;
// contatori diagnostici
int _countAll = 0;
int _countVisible = 0; // trashed=0
int _countTrashed = 0; // trashed=1
@override
void initState() {
super.initState();
_init(); // prepara baseUrl + header auth (se necessari), poi carica i dati
}
Future<void> _init() async {
// 1) Base URL: parametro > settings
final s = await RemoteSettings.load();
final candidate = (widget.baseUrl ?? '').trim();
_baseUrl = candidate.isNotEmpty ? candidate : s.baseUrl.trim();
// 2) Header Authorization (opzionale)
_authHeaders = null;
try {
if (_baseUrl.isNotEmpty && (s.email.isNotEmpty || s.password.isNotEmpty)) {
final auth = RemoteAuth(baseUrl: _baseUrl, email: s.email, password: s.password);
final token = await auth.login();
_authHeaders = {'Authorization': 'Bearer $token'};
}
} catch (_) {
// In debug non bloccare la pagina se il login immagini fallisce
_authHeaders = null;
}
// 3) Carica contatori e lista
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
}
Future<void> _refreshCounters() async {
// Totale remoti (origin=1), visibili e cestinati
final all = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1",
);
final vis = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=0",
);
final tra = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=1",
);
_countAll = (all.first['c'] as int?) ?? 0;
_countVisible = (vis.first['c'] as int?) ?? 0;
_countTrashed = (tra.first['c'] as int?) ?? 0;
}
Future<List<_RemoteRow>> _load() async {
// Filtro WHERE in base al toggle
String extraWhere = '';
switch (_filter) {
case _RemoteFilter.visibleOnly:
extraWhere = ' AND trashed=0';
break;
case _RemoteFilter.trashedOnly:
extraWhere = ' AND trashed=1';
break;
case _RemoteFilter.all:
default:
extraWhere = '';
}
// Prende le prime 300 entry remote (includiamo il mime e il remoteId)
final rows = await widget.db.rawQuery(
'SELECT id, remoteId, title, remotePath, remoteThumb2, sourceMimeType, trashed '
'FROM entry WHERE origin=1$extraWhere '
'ORDER BY id DESC LIMIT 300',
);
return rows.map((r) {
return _RemoteRow(
id: r['id'] as int,
remoteId: (r['remoteId'] as String?) ?? '',
title: (r['title'] as String?) ?? '',
remotePath: r['remotePath'] as String?,
remoteThumb2: r['remoteThumb2'] as String?,
mime: r['sourceMimeType'] as String?,
trashed: (r['trashed'] as int?) ?? 0,
);
}).toList();
}
// Costruzione robusta dellURL assoluto:
// - se già assoluto ritorna comè
// - se relativo risolve contro _baseUrl (accetta con/senza '/')
String _absUrl(String? relativePath) {
if (relativePath == null || relativePath.isEmpty) return '';
final p = relativePath.trim();
// URL già assoluto
if (p.startsWith('http://') || p.startsWith('https://')) return p;
if (_baseUrl.isEmpty) return '';
try {
final base = Uri.parse(_baseUrl.endsWith('/') ? _baseUrl : '$_baseUrl/');
// normalizza: se inizia con '/', togliamo per usare resolve coerente
final rel = p.startsWith('/') ? p.substring(1) : p;
final resolved = base.resolve(rel);
return resolved.toString();
} catch (_) {
return '';
}
}
bool _isVideo(String? mime, String? path) {
final m = (mime ?? '').toLowerCase();
final p = (path ?? '').toLowerCase();
return m.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Future<void> _onRefresh() async {
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
await _future;
}
Future<void> _diagnosticaDb() async {
try {
final dup = await widget.db.rawQuery('''
SELECT remoteId, COUNT(*) AS cnt
FROM entry
WHERE origin=1 AND remoteId IS NOT NULL
GROUP BY remoteId
HAVING cnt > 1
''');
final vis = await widget.db.rawQuery('''
SELECT COUNT(*) AS visible_remotes
FROM entry
WHERE origin=1 AND trashed=0
''');
final idx = await widget.db.rawQuery("PRAGMA index_list('entry')");
if (!mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Diagnostica DB', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('Duplicati per remoteId:\n${dup.isEmpty ? "nessuno" : dup.map((e)=>e.toString()).join('\n')}'),
const SizedBox(height: 12),
Text('Remoti visibili in Aves (trashed=0): ${vis.first.values.first}'),
const SizedBox(height: 12),
Text('Indici su entry:\n${idx.map((e)=>e.toString()).join('\n')}'),
],
),
),
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Diagnostica DB fallita: $e')),
);
}
}
/// 🔧 Pulisce duplicati per `remotePath` (tiene MAX(id)) e righe senza `remoteId`.
Future<void> _pulisciDuplicatiPath() async {
try {
final delNoId = await widget.db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
final delByPath = await widget.db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
await _onRefresh();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia completata: noId=$delNoId, dupPath=$delByPath')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia fallita: $e')),
);
}
}
Future<void> _nascondiRemotiInCollection() async {
try {
final changed = await widget.db.rawUpdate('''
UPDATE entry SET trashed=1
WHERE origin=1 AND trashed=0
''');
if (!mounted) return;
await _onRefresh();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Remoti nascosti dalla Collection: $changed')),
);
} on DatabaseException catch (e) {
final msg = e.toString();
if (!mounted) return;
// Probabile connessione R/O: istruisci a riaprire il DB in R/W
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
'UPDATE fallito (DB in sola lettura?): $msg\n'
'Apri il DB in R/W in HomePage._openRemoteTestPage (no readOnly).',
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore UPDATE: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final ready = (_baseUrl.isNotEmpty && _future != null);
return Scaffold(
appBar: AppBar(
title: const Text('[DEBUG] Remote Test'),
actions: [
IconButton(
icon: const Icon(Icons.bug_report_outlined),
tooltip: 'Diagnostica DB',
onPressed: _diagnosticaDb,
),
IconButton(
icon: const Icon(Icons.cleaning_services_outlined),
tooltip: 'Pulisci duplicati (path)',
onPressed: _pulisciDuplicatiPath,
),
IconButton(
icon: const Icon(Icons.visibility_off_outlined),
tooltip: 'Nascondi remoti in Collection',
onPressed: _nascondiRemotiInCollection,
),
],
),
body: !ready
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// Header contatori + filtro
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 8,
runSpacing: -6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(label: Text('Tot: $_countAll')),
Chip(label: Text('Visibili: $_countVisible')),
Chip(label: Text('Cestinati: $_countTrashed')),
],
),
),
const SizedBox(width: 8),
SegmentedButton<_RemoteFilter>(
segments: const [
ButtonSegment(value: _RemoteFilter.all, label: Text('Tutti')),
ButtonSegment(value: _RemoteFilter.visibleOnly, label: Text('Visibili')),
ButtonSegment(value: _RemoteFilter.trashedOnly, label: Text('Cestinati')),
],
selected: {_filter},
onSelectionChanged: (sel) async {
setState(() => _filter = sel.first);
await _onRefresh();
},
),
],
),
),
const Divider(height: 1),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: FutureBuilder<List<_RemoteRow>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: Center(child: Text('Errore: ${snap.error}')),
),
);
}
final items = snap.data ?? const <_RemoteRow>[];
if (items.isEmpty) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: const Center(child: Text('Nessuna entry remota (origin=1)')),
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, mainAxisSpacing: 4, crossAxisSpacing: 4,
),
itemCount: items.length,
itemBuilder: (context, i) {
final it = items[i];
final isVideo = _isVideo(it.mime, it.remotePath);
final thumbUrl = _absUrl(it.remoteThumb2);
final fullUrl = _absUrl(it.remotePath);
final hasThumb = thumbUrl.isNotEmpty;
final hasFull = fullUrl.isNotEmpty;
final heroTag = 'remote_${it.id}';
return GestureDetector(
onLongPress: () async {
if (!context.mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${it.id} remoteId: ${it.remoteId} trashed: ${it.trashed}'),
const SizedBox(height: 8),
Text('MIME: ${it.mime}'),
const Divider(),
SelectableText('FULL URL:\n$fullUrl'),
const SizedBox(height: 8),
SelectableText('THUMB URL:\n$thumbUrl'),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
ElevatedButton.icon(
onPressed: hasFull
? () async {
await Clipboard.setData(ClipboardData(text: fullUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('FULL URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy),
label: const Text('Copia FULL'),
),
ElevatedButton.icon(
onPressed: hasThumb
? () async {
await Clipboard.setData(ClipboardData(text: thumbUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('THUMB URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy_all),
label: const Text('Copia THUMB'),
),
],
),
],
),
),
),
),
);
},
onTap: () async {
if (_navigating) return; // debounce
_navigating = true;
try {
if (isVideo) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video remoto: anteprima full non disponibile (thumb richiesta).'),
duration: Duration(seconds: 2),
),
);
return;
}
if (!hasFull) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL non valido')),
);
return;
}
await Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (_, __, ___) => _RemoteFullPage(
title: it.title,
url: fullUrl,
headers: _authHeaders,
heroTag: heroTag, // pairing Hero
),
transitionDuration: const Duration(milliseconds: 220),
),
);
} finally {
_navigating = false;
}
},
child: Hero(
tag: heroTag, // pairing Hero
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all(color: Colors.black12)),
child: Stack(
fit: StackFit.expand,
children: [
_buildGridTile(isVideo, thumbUrl, fullUrl),
// Informazioni utili per capire cosa stiamo vedendo
Positioned(
left: 2,
bottom: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
color: Colors.black54,
child: Text(
'id:${it.id} rid:${it.remoteId}${it.trashed==1 ? " (T)" : ""}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
Positioned(
right: 2,
top: 2,
child: Wrap(
spacing: 4,
children: [
if (hasFull)
const _MiniBadge(label: 'URL')
else
const _MiniBadge(label: 'NOURL', color: Colors.red),
if (hasThumb)
const _MiniBadge(label: 'THUMB')
else
const _MiniBadge(label: 'NOTH', color: Colors.orange),
],
),
),
],
),
),
),
);
},
);
},
),
),
),
],
),
);
}
Widget _buildGridTile(bool isVideo, String thumbUrl, String fullUrl) {
if (isVideo) {
// Per i video: NON usiamo Image.network(fullUrl).
// Usiamo la thumb se c'è, altrimenti placeholder con icona "play".
final base = thumbUrl.isEmpty
? const ColoredBox(color: Colors.black12)
: Image.network(
thumbUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
return Stack(
fit: StackFit.expand,
children: [
base,
const Align(
alignment: Alignment.center,
child: Icon(Icons.play_circle_fill, color: Colors.white70, size: 48),
),
],
);
}
// Per le immagini: se non c'è thumb, posso usare direttamente l'URL full.
final displayUrl = thumbUrl.isEmpty ? fullUrl : thumbUrl;
if (displayUrl.isEmpty) {
return const ColoredBox(color: Colors.black12);
}
return Image.network(
displayUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
}
}
class _RemoteRow {
final int id;
final String remoteId;
final String title;
final String? remotePath;
final String? remoteThumb2;
final String? mime;
final int trashed;
_RemoteRow({
required this.id,
required this.remoteId,
required this.title,
this.remotePath,
this.remoteThumb2,
this.mime,
required this.trashed,
});
}
class _MiniBadge extends StatelessWidget {
final String label;
final Color color;
const _MiniBadge({super.key, required this.label, this.color = Colors.black54});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(3),
),
child: Text(label, style: const TextStyle(fontSize: 9, color: Colors.white)),
);
}
}
class _RemoteFullPage extends StatelessWidget {
final String title;
final String url;
final Map<String, String>? headers;
final String heroTag; // pairing Hero
const _RemoteFullPage({
super.key,
required this.title,
required this.url,
required this.heroTag,
this.headers,
});
@override
Widget build(BuildContext context) {
final body = url.isEmpty
? const Text('URL non valido')
: Hero(
tag: heroTag, // pairing con la griglia
child: InteractiveViewer(
maxScale: 5,
child: Image.network(
url,
fit: BoxFit.contain,
headers: headers, // Authorization se il server lo richiede
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 64),
),
),
);
return Scaffold(
appBar: AppBar(title: Text(title.isEmpty ? 'Remote' : title)),
body: Center(child: body),
);
}
}

View file

@ -1,194 +0,0 @@
// lib/remote/run_remote_sync.dart
//
// Esegue un ciclo di sincronizzazione "pull":
// 1) legge le impostazioni (server, path, user, password) da RemoteSettings
// 2) login Bearer token
// 3) GET dell'indice JSON (array di oggetti foto)
// 4) upsert nel DB 'entry' (e 'address' se presente) tramite RemoteRepository
//
// NOTE:
// - La versione "managed" (runRemoteSyncOnceManaged) apre/chiude il DB ed evita run concorrenti.
// - La versione "plain" (runRemoteSyncOnce) usa un Database già aperto (compatibilità).
// - PRAGMA per concorrenza (WAL, busy_timeout, ...).
// - Non logghiamo contenuti sensibili (password/token/body completi).
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
import 'remote_client.dart';
import 'remote_repository.dart';
// === Guardia anti-concorrenza (single-flight) per la run "managed" ===
bool _remoteSyncRunning = false;
/// Helper: retry esponenziale breve per SQLITE_BUSY.
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
final msg = e.toString();
final isBusy = msg.contains('SQLITE_BUSY') || msg.contains('database is locked');
if (!isBusy || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
/// Versione "managed":
/// - impedisce run concorrenti
/// - apre/chiude da sola la connessione a `metadata.db` (istanza indipendente)
/// - imposta PRAGMA per concorrenza
/// - accetta override opzionali (utile in test)
Future<void> runRemoteSyncOnceManaged({
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
if (_remoteSyncRunning) {
// ignore: avoid_print
print('[remote-sync] already running, skip');
return;
}
_remoteSyncRunning = true;
Database? db;
try {
final dbDir = await getDatabasesPath();
final dbPath = p.join(dbDir, 'metadata.db');
db = await openDatabase(
dbPath,
singleInstance: false, // connessione indipendente (non chiude lhandle di Aves)
onConfigure: (db) async {
try {
// Alcuni PRAGMA ritornano valori usare SEMPRE rawQuery.
await db.rawQuery('PRAGMA journal_mode=WAL');
await db.rawQuery('PRAGMA synchronous=NORMAL');
await db.rawQuery('PRAGMA busy_timeout=3000');
await db.rawQuery('PRAGMA wal_autocheckpoint=1000');
await db.rawQuery('PRAGMA foreign_keys=ON');
// (Opzionale) verifica del mode corrente
final jm = await db.rawQuery('PRAGMA journal_mode');
final mode = jm.isNotEmpty ? jm.first.values.first : null;
// ignore: avoid_print
print('[remote-sync] journal_mode=$mode'); // atteso: wal
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][WARN] PRAGMA setup failed: $e\n$st');
// Non rilanciare: in estremo, continueremo con journaling di default
}
},
);
await runRemoteSyncOnce(
db: db,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
} finally {
try {
await db?.close();
} catch (_) {
// In caso di close doppio/già chiuso, ignoro.
}
_remoteSyncRunning = false;
}
}
/// Versione "plain":
/// Esegue login, scarica /photos e fa upsert nel DB usando una connessione
/// SQLite **già aperta** (non viene chiusa qui).
///
/// Gli optional [baseUrl], [indexPath], [email], [password] permettono override
/// delle impostazioni salvate in `RemoteSettings` (comodo per test / debug).
Future<void> runRemoteSyncOnce({
required Database db,
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
try {
// 1) Carica impostazioni sicure (secure storage)
final s = await RemoteSettings.load();
final bUrl = (baseUrl ?? s.baseUrl).trim();
final ip = (indexPath ?? s.indexPath).trim();
final em = (email ?? s.email).trim();
final pw = (password ?? s.password);
if (bUrl.isEmpty || ip.isEmpty) {
throw StateError('Impostazioni remote incomplete: baseUrl/indexPath mancanti');
}
// 2) Autenticazione (Bearer)
final auth = RemoteAuth(baseUrl: bUrl, email: em, password: pw);
await auth.login(); // Se necessario, RemoteJsonClient può riloggare su 401
// 3) Client JSON (segue anche redirect 301/302/307/308)
final client = RemoteJsonClient(bUrl, ip, auth: auth);
// 4) Scarica lelenco di elementi remoti (array top-level)
final items = await client.fetchAll();
// 5) Upsert nel DB (con retry se incappiamo in SQLITE_BUSY)
final repo = RemoteRepository(db);
await _withRetryBusy(() => repo.upsertAll(items));
// 5.b) Impedisci futuri duplicati e ripulisci quelli già presenti
await repo.ensureUniqueRemoteId();
final removed = await repo.deduplicateRemotes();
// 5.c) Paracadute: assicura che i remoti NON siano mostrati nella Collection Aves
//await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;');
// 5.d) **CLEANUP LEGACY**: elimina righe remote "orfane" o doppioni su remotePath
// - Righe senza remoteId (NULL o vuoto): non deduplicabili via UNIQUE vanno rimosse
final purgedNoId = await db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
// - Doppioni per remotePath: tieni solo la riga con id MAX
// (copre i casi in cui in passato siano state create due righe per lo stesso path)
final purgedByPath = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
// ignore: avoid_print
print('[remote-sync] cleanup: removed dup(remoteId)=$removed, purged(noId)=$purgedNoId, purged(byPath)=$purgedByPath');
// 6) Log sintetico
int? c;
try {
c = await repo.countRemote();
} catch (_) {
c = null;
}
// ignore: avoid_print
if (c == null) {
print('[remote-sync] import completato (conteggio non disponibile)');
} else {
print('[remote-sync] importati remoti: $c (base=$bUrl, index=$ip)');
}
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][ERROR] $e\n$st');
rethrow;
}
}

View file

@ -1,7 +0,0 @@
// lib/remote/url_utils.dart
Uri buildAbsoluteUri(String baseUrl, String relativePath) {
final base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
final cleaned = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
final segments = cleaned.split('/').where((s) => s.isNotEmpty).toList();
return base.replace(pathSegments: [...base.pathSegments, ...segments]);
}

View file

@ -1,96 +0,0 @@
// lib/remote/auth_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
/// Gestisce autenticazione remota e caching del Bearer token.
/// - [baseUrl]: URL base del server (con o senza '/')
/// - [email]/[password]: credenziali
/// - [loginPath]: path dell'endpoint di login (default 'auth/login')
/// - [timeout]: timeout per le richieste (default 20s)
class RemoteAuth {
final Uri base;
final String email;
final String password;
final String loginPath;
final Duration timeout;
String? _token;
RemoteAuth({
required String baseUrl,
required this.email,
required this.password,
this.loginPath = 'auth/login',
this.timeout = const Duration(seconds: 20),
}) : base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
Uri get _loginUri => base.resolve(loginPath);
/// Esegue il login e memorizza il token.
/// Lancia eccezione con messaggio chiaro in caso di errore HTTP, rete o JSON.
Future<String> login() async {
final uri = _loginUri;
final headers = {'Content-Type': 'application/json'};
final bodyStr = json.encode({'email': email, 'password': password});
http.Response res;
try {
res = await http
.post(uri, headers: headers, body: bodyStr)
.timeout(timeout);
} catch (e) {
throw Exception('Login fallito: errore di rete verso $uri: $e');
}
// Follow esplicito per redirect POST moderni (307/308) mantenendo metodo e body
if ({307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
final redirectUri = uri.resolve(res.headers['location']!);
try {
res = await http
.post(redirectUri, headers: headers, body: bodyStr)
.timeout(timeout);
} catch (e) {
throw Exception('Login fallito: errore di rete verso $redirectUri: $e');
}
}
if (res.statusCode != 200) {
final snippet = utf8.decode(res.bodyBytes.take(200).toList());
throw Exception(
'Login fallito: HTTP ${res.statusCode} ${res.reasonPhrase} $snippet',
);
}
// Parsing JSON robusto
Map<String, dynamic> map;
try {
map = json.decode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
} catch (_) {
throw Exception('Login fallito: risposta non è un JSON valido');
}
// Supporto sia 'token' sia 'access_token'
final token = (map['token'] ?? map['access_token']) as String?;
if (token == null || token.isEmpty) {
throw Exception('Login fallito: token assente nella risposta');
}
_token = token;
return token;
}
/// Ritorna gli header con Bearer; se non hai token, esegue login.
Future<Map<String, String>> authHeaders() async {
_token ??= await login();
return {'Authorization': 'Bearer $_token'};
}
/// Forza il rinnovo del token (es. dopo 401) e ritorna i nuovi header.
Future<Map<String, String>> refreshAndHeaders() async {
_token = null;
return await authHeaders();
}
/// Accesso in sola lettura al token corrente (può essere null).
String? get token => _token;
}

View file

@ -1,93 +0,0 @@
// lib/remote/remote_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'remote_models.dart';
import 'auth_client.dart';
class RemoteJsonClient {
final Uri indexUri; // es. https://prova.patachina.it/photos/
final RemoteAuth? auth; // opzionale: se presente, aggiunge Bearer
RemoteJsonClient(
String baseUrl,
String indexPath, {
this.auth,
}) : indexUri = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/')
.resolve(indexPath);
Future<List<RemotePhotoItem>> fetchAll() async {
Map<String, String> headers = {};
if (auth != null) {
headers = await auth!.authHeaders();
}
// DEBUG: stampa la URL precisa
// ignore: avoid_print
print('[remote-client] GET $indexUri');
http.Response res;
try {
res = await http.get(indexUri, headers: headers).timeout(const Duration(seconds: 20));
} catch (e) {
throw Exception('Errore rete su $indexUri: $e');
}
// Retry 1 volta in caso di 401 (token scaduto/invalidato)
if (res.statusCode == 401 && auth != null) {
headers = await auth!.refreshAndHeaders();
res = await http.get(indexUri, headers: headers).timeout(const Duration(seconds: 20));
}
// Follow 30x mantenendo Authorization
if ({301, 302, 307, 308}.contains(res.statusCode) && res.headers['location'] != null) {
final loc = res.headers['location']!;
final redirectUri = indexUri.resolve(loc);
res = await http.get(redirectUri, headers: headers).timeout(const Duration(seconds: 20));
}
if (res.statusCode != 200) {
final snippet = utf8.decode(res.bodyBytes.take(200).toList());
throw Exception('HTTP ${res.statusCode} ${res.reasonPhrase} su $indexUri. Body: $snippet');
}
final body = utf8.decode(res.bodyBytes);
// Qui siamo espliciti: ci aspettiamo SEMPRE una lista top-level
final dynamic decoded = json.decode(body);
if (decoded is! List) {
throw Exception('JSON inatteso: atteso array top-level, ricevuto ${decoded.runtimeType}');
}
final List<dynamic> rawList = decoded;
// --- DIAGNOSTICA: conteggio pattern dai dati del SERVER (non stampo il JSON intero)
int withOriginal = 0, withoutOriginal = 0, leadingSlash = 0, noLeadingSlash = 0;
for (final e in rawList) {
if (e is Map<String, dynamic>) {
final p = (e['path'] ?? '').toString();
if (p.startsWith('/')) {
leadingSlash++;
} else {
noLeadingSlash++;
}
if (p.contains('/original/')) {
withOriginal++;
} else {
withoutOriginal++;
}
}
}
// ignore: avoid_print
print('[remote-client] SERVER paths: withOriginal=$withOriginal '
'withoutOriginal=$withoutOriginal leadingSlash=$leadingSlash noLeadingSlash=$noLeadingSlash');
// Costruiamo a mano la List<RemotePhotoItem>, tipizzata esplicitamente
final List<RemotePhotoItem> items = rawList.map<RemotePhotoItem>((e) {
if (e is! Map<String, dynamic>) {
throw Exception('Elemento JSON non è una mappa: ${e.runtimeType} -> $e');
}
return RemotePhotoItem.fromJson(e);
}).toList();
return items;
}
}

View file

@ -1,125 +0,0 @@
// lib/remote/remote_models.dart
import 'url_utils.dart';
class RemotePhotoItem {
final String id;
final String name;
final String path;
final String? thub1, thub2;
final String? mimeType;
final int? width, height, sizeBytes;
final DateTime? takenAtUtc;
final double? lat, lng, alt;
final String? dataExifLegacy;
final String? user;
final int? durationMillis;
final RemoteLocation? location;
RemotePhotoItem({
required this.id,
required this.name,
required this.path,
this.thub1,
this.thub2,
this.mimeType,
this.width,
this.height,
this.sizeBytes,
this.takenAtUtc,
this.lat,
this.lng,
this.alt,
this.dataExifLegacy,
this.user,
this.durationMillis,
this.location,
});
// URL completo costruito solo in fase di lettura
// String get uri => "https://prova.patachina.it/$path";
// Costruzione URL assoluto delegata a utility (in base alle impostazioni)
String absoluteUrl(String baseUrl) => buildAbsoluteUri(baseUrl, path).toString();
static DateTime? _tryParseIsoUtc(dynamic v) {
if (v == null) return null;
try { return DateTime.parse(v.toString()).toUtc(); } catch (_) { return null; }
}
static double? _toDouble(dynamic v) {
if (v == null) return null;
if (v is num) return v.toDouble();
return double.tryParse(v.toString());
}
static int? _toMillis(dynamic v) {
if (v == null) return null;
final num? n = (v is num) ? v : num.tryParse(v.toString());
if (n == null) return null;
return n >= 1000 ? n.toInt() : (n * 1000).toInt();
}
factory RemotePhotoItem.fromJson(Map<String, dynamic> j) {
final gps = j['gps'] as Map<String, dynamic>?;
final loc = j['location'] is Map<String, dynamic>
? RemoteLocation.fromJson(j['location'] as Map<String, dynamic>)
: null;
return RemotePhotoItem(
id: (j['id'] ?? j['name']).toString(),
name: (j['name'] ?? '').toString(),
path: (j['path'] ?? '').toString(),
thub1: j['thub1']?.toString(),
thub2: j['thub2']?.toString(),
mimeType: j['mime_type']?.toString(),
width: (j['width'] as num?)?.toInt(),
height: (j['height'] as num?)?.toInt(),
sizeBytes: (j['size_bytes'] as num?)?.toInt(),
takenAtUtc: _tryParseIsoUtc(j['taken_at']),
dataExifLegacy: j['data']?.toString(),
lat: gps != null ? _toDouble(gps['lat']) : null,
lng: gps != null ? _toDouble(gps['lng']) : null,
alt: gps != null ? _toDouble(gps['alt']) : null,
user: j['user']?.toString(),
durationMillis: _toMillis(j['duration']),
location: loc,
);
}
}
class RemoteLocation {
final String? continent;
final String? country;
final String? region;
final String? postcode;
final String? city;
final String? countyCode;
final String? address;
final String? timezone;
final String? timeOffset;
RemoteLocation({
this.continent,
this.country,
this.region,
this.postcode,
this.city,
this.countyCode,
this.address,
this.timezone,
this.timeOffset,
});
factory RemoteLocation.fromJson(Map<String, dynamic> j) => RemoteLocation(
continent: j['continent']?.toString(),
country: j['country']?.toString(),
region: j['region']?.toString(),
postcode: j['postcode']?.toString(),
city: j['city']?.toString(),
countyCode:j['county_code']?.toString(),
address: j['address']?.toString(),
timezone: j['timezone']?.toString(),
timeOffset:j['time']?.toString(),
);
}

View file

@ -1,413 +0,0 @@
// lib/remote/remote_repository.dart
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:sqflite/sqflite.dart';
import 'remote_models.dart';
class RemoteRepository {
final Database db;
RemoteRepository(this.db);
// =========================
// Helpers PRAGMA / schema
// =========================
Future<void> _ensureColumns(
DatabaseExecutor dbExec, {
required String table,
required Map<String, String> columnsAndTypes,
}) async {
try {
final rows = await dbExec.rawQuery('PRAGMA table_info($table);');
final existing = rows.map((r) => (r['name'] as String)).toSet();
for (final entry in columnsAndTypes.entries) {
final col = entry.key;
final typ = entry.value;
if (!existing.contains(col)) {
final sql = 'ALTER TABLE $table ADD COLUMN $col $typ;';
try {
await dbExec.execute(sql);
debugPrint('[RemoteRepository] executed: $sql');
} catch (e, st) {
debugPrint('[RemoteRepository] failed to execute $sql: $e\n$st');
}
}
}
} catch (e, st) {
debugPrint('[RemoteRepository] _ensureColumns($table) error: $e\n$st');
}
}
/// Assicura che le colonne GPS e alcune colonne "remote*" esistano nella tabella `entry`.
Future<void> _ensureEntryColumns(DatabaseExecutor dbExec) async {
await _ensureColumns(dbExec, table: 'entry', columnsAndTypes: const {
// GPS
'latitude': 'REAL',
'longitude': 'REAL',
'altitude': 'REAL',
// Campi remoti
'remoteId': 'TEXT',
'remotePath': 'TEXT',
'remoteThumb1': 'TEXT',
'remoteThumb2': 'TEXT',
'origin': 'INTEGER',
'provider': 'TEXT',
'trashed': 'INTEGER',
});
// Indice "normale" per velocizzare il lookup su remoteId
try {
await dbExec.execute(
'CREATE INDEX IF NOT EXISTS idx_entry_remoteId ON entry(remoteId);',
);
} catch (e, st) {
debugPrint('[RemoteRepository] create index error: $e\n$st');
}
}
// =========================
// Retry su SQLITE_BUSY
// =========================
bool _isBusy(Object e) {
final s = e.toString();
return s.contains('SQLITE_BUSY') || s.contains('database is locked');
}
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
if (!_isBusy(e) || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
// =========================
// Normalizzazione / Canonicalizzazione
// =========================
/// Normalizza gli slash e forza lo slash iniziale.
String _normPath(String? p) {
if (p == null || p.isEmpty) return '';
var s = p.trim().replaceAll(RegExp(r'/+'), '/');
if (!s.startsWith('/')) s = '/$s';
return s;
}
/// Inserisce '/original/' dopo '/photos/<User>/' se manca, e garantisce filename coerente.
String _canonFullPath(String? rawPath, String fileName) {
var s = _normPath(rawPath);
final seg = s.split('/'); // ['', 'photos', '<User>', maybe 'original', ...]
if (seg.length >= 4 && seg[1] == 'photos' && seg[3] != 'original' && seg[3] != 'thumbs') {
seg.insert(3, 'original');
}
// forza il filename finale (se fornito)
if (fileName.isNotEmpty) {
seg[seg.length - 1] = fileName;
}
return seg.join('/');
}
// =========================
// Utilities
// =========================
bool _isVideoItem(RemotePhotoItem it) {
final mt = (it.mimeType ?? '').toLowerCase();
final p = (it.path).toLowerCase();
return mt.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Map<String, Object?> _buildEntryRow(RemotePhotoItem it, {int? existingId}) {
final canonical = _canonFullPath(it.path, it.name);
final thumb = _normPath(it.thub2);
return <String, Object?>{
'id': existingId,
'contentId': null,
'uri': null,
'path': canonical, // path interno
'sourceMimeType': it.mimeType,
'width': it.width,
'height': it.height,
'sourceRotationDegrees': null,
'sizeBytes': it.sizeBytes,
'title': it.name,
'dateAddedSecs': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'dateModifiedMillis': null,
'sourceDateTakenMillis': it.takenAtUtc?.millisecondsSinceEpoch,
'durationMillis': it.durationMillis,
// 👇 REMOTI visibili nella Collection (se la tua Collection include origin=1)
'trashed': 0,
'origin': 1,
'provider': 'json@patachina',
// GPS (possono essere null)
'latitude': it.lat,
'longitude': it.lng,
'altitude': it.alt,
// campi remoti
'remoteId': it.id,
'remotePath': canonical, // <-- sempre canonico con /original/
'remoteThumb1': it.thub1,
'remoteThumb2': thumb,
};
}
Map<String, Object?> _buildAddressRow(int newId, RemoteLocation location) {
return <String, Object?>{
'id': newId,
'addressLine': location.address,
'countryCode': null,
'countryName': location.country,
'adminArea': location.region,
'locality': location.city,
};
}
// =========================
// Upsert a chunk
// =========================
/// Inserisce o aggiorna tutti gli elementi remoti.
///
/// - Assicura colonne `entry` (GPS + remote*)
/// - Canonicalizza i path (`/photos/<User>/original/...`)
/// - Lookup robusto: remoteId -> remotePath(canonico) -> remotePath(raw normalizzato)
/// - Ordina prima le immagini, poi i video
/// - In caso di errore schema su GPS, riprova senza i 3 campi GPS
Future<void> upsertAll(List<RemotePhotoItem> items, {int chunkSize = 200}) async {
debugPrint('RemoteRepository.upsertAll: items=${items.length}');
if (items.isEmpty) return;
// Garantisco lo schema una volta, poi procedo ai chunk
await _withRetryBusy(() => _ensureEntryColumns(db));
// Indici UNIQUE per prevenire futuri duplicati (id + path)
await ensureUniqueRemoteId();
await ensureUniqueRemotePath();
// Ordina: prima immagini, poi video
final images = <RemotePhotoItem>[];
final videos = <RemotePhotoItem>[];
for (final it in items) {
(_isVideoItem(it) ? videos : images).add(it);
}
final ordered = <RemotePhotoItem>[...images, ...videos];
for (var offset = 0; offset < ordered.length; offset += chunkSize) {
final end = (offset + chunkSize < ordered.length) ? offset + chunkSize : ordered.length;
final chunk = ordered.sublist(offset, end);
try {
await _withRetryBusy(() => db.transaction((txn) async {
final batch = txn.batch();
for (final it in chunk) {
// Lookup record esistente per stabilire l'ID (REPLACE mantiene la PK)
int? existingId;
// 1) prova per remoteId
try {
final existing = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remoteId = ?',
whereArgs: [it.id],
limit: 1,
);
if (existing.isNotEmpty) {
existingId = existing.first['id'] as int?;
} else {
// 2) fallback per remotePath canonico
final canonical = _canonFullPath(it.path, it.name);
final byCanon = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [canonical],
limit: 1,
);
if (byCanon.isNotEmpty) {
existingId = byCanon.first['id'] as int?;
} else {
// 3) ultimo fallback: remotePath "raw normalizzato" (senza forzare /original/)
final rawNorm = _normPath(it.path);
final byRaw = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [rawNorm],
limit: 1,
);
if (byRaw.isNotEmpty) {
existingId = byRaw.first['id'] as int?;
}
}
}
} catch (e, st) {
debugPrint('[RemoteRepository] lookup existingId failed for remoteId=${it.id}: $e\n$st');
}
// Riga completa (con path canonico)
final row = _buildEntryRow(it, existingId: existingId);
// Provo insert/replace con i campi completi (GPS inclusi)
try {
batch.insert(
'entry',
row,
conflictAlgorithm: ConflictAlgorithm.replace,
);
// Address: lo inseriamo in un secondo pass (post-commit) con PK certa
} on DatabaseException catch (e, st) {
// Se fallisce per schema GPS (colonne non create o tipo non compatibile), riprovo senza i 3 campi
debugPrint('[RemoteRepository] batch insert failed for remoteId=${it.id}: $e\n$st');
final rowNoGps = Map<String, Object?>.from(row)
..remove('latitude')
..remove('longitude')
..remove('altitude');
batch.insert(
'entry',
rowNoGps,
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
await batch.commit(noResult: true);
// Secondo pass per address, con PK certa
for (final it in chunk) {
if (it.location == null) continue;
try {
// cerco per remoteId, altrimenti per path canonico
final canonical = _canonFullPath(it.path, it.name);
final rows = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND (remoteId = ? OR remotePath = ?)',
whereArgs: [it.id, canonical],
limit: 1,
);
if (rows.isEmpty) continue;
final newId = rows.first['id'] as int;
final addr = _buildAddressRow(newId, it.location!);
await txn.insert(
'address',
addr,
conflictAlgorithm: ConflictAlgorithm.replace,
);
} catch (e, st) {
debugPrint('[RemoteRepository] insert address failed for remoteId=${it.id}: $e\n$st');
}
}
}));
} catch (e, st) {
debugPrint('[RemoteRepository] upsert chunk ${offset}..${end - 1} ERROR: $e\n$st');
rethrow;
}
}
}
// =========================
// Unicità & deduplica
// =========================
/// Crea un indice UNICO su `remoteId` limitato alle righe remote (origin=1).
Future<void> ensureUniqueRemoteId() async {
try {
await db.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_entry_remote_remoteId '
'ON entry(remoteId) WHERE origin=1',
);
debugPrint('[RemoteRepository] ensured UNIQUE index on entry(remoteId) for origin=1');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureUniqueRemoteId error: $e\n$st');
}
}
/// Crea un indice UNICO su `remotePath` (solo remoti) per prevenire doppi.
Future<void> ensureUniqueRemotePath() async {
try {
await db.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_entry_remote_remotePath '
'ON entry(remotePath) WHERE origin=1 AND remotePath IS NOT NULL',
);
debugPrint('[RemoteRepository] ensured UNIQUE index on entry(remotePath) for origin=1');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureUniqueRemotePath error: $e\n$st');
}
}
/// Rimuove duplicati remoti, tenendo la riga con id MAX per ciascun `remoteId`.
Future<int> deduplicateRemotes() async {
try {
final deleted = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remoteId IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remoteId IS NOT NULL '
' GROUP BY remoteId'
')',
);
debugPrint('[RemoteRepository] deduplicateRemotes deleted=$deleted');
return deleted;
} catch (e, st) {
debugPrint('[RemoteRepository] deduplicateRemotes error: $e\n$st');
return 0;
}
}
/// Rimuove duplicati per `remotePath` (exact match), tenendo l'ultima riga.
Future<int> deduplicateByRemotePath() async {
try {
final deleted = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
debugPrint('[RemoteRepository] deduplicateByRemotePath deleted=$deleted');
return deleted;
} catch (e, st) {
debugPrint('[RemoteRepository] deduplicateByRemotePath error: $e\n$st');
return 0;
}
}
/// Helper combinato: prima pulisce i doppioni, poi impone lunicità.
Future<void> sanitizeRemotes() async {
await deduplicateRemotes();
await deduplicateByRemotePath();
await ensureUniqueRemoteId();
await ensureUniqueRemotePath();
}
// =========================
// Utils
// =========================
Future<int> countRemote() async {
final rows = await db.rawQuery('SELECT COUNT(1) AS c FROM entry WHERE origin=1');
return (rows.first['c'] as int?) ?? 0;
}
}

View file

@ -1,83 +0,0 @@
// lib/remote/remote_settings.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class RemoteSettings {
static const _storage = FlutterSecureStorage();
// Keys
static const _kEnabled = 'remote_enabled';
static const _kBaseUrl = 'remote_base_url';
static const _kIndexPath = 'remote_index_path';
static const _kEmail = 'remote_email';
static const _kPassword = 'remote_password';
// Default values:
// In DEBUG vogliamo valori pre-compilati; in RELEASE lasciamo vuoti/false.
static final bool defaultEnabled = kDebugMode ? true : false;
static final String defaultBaseUrl = kDebugMode ? 'https://prova.patachina.it' : '';
static final String defaultIndexPath = kDebugMode ? 'photos/' : '';
static final String defaultEmail = kDebugMode ? 'fabio@gmail.com' : '';
static final String defaultPassword = kDebugMode ? 'master66' : '';
bool enabled;
String baseUrl;
String indexPath;
String email;
String password;
RemoteSettings({
required this.enabled,
required this.baseUrl,
required this.indexPath,
required this.email,
required this.password,
});
/// Carica i setting dal secure storage.
/// Se un valore non esiste, usa i default (in debug: quelli precompilati).
static Future<RemoteSettings> load() async {
final enabledStr = await _storage.read(key: _kEnabled);
final baseUrl = await _storage.read(key: _kBaseUrl) ?? defaultBaseUrl;
final indexPath = await _storage.read(key: _kIndexPath) ?? defaultIndexPath;
final email = await _storage.read(key: _kEmail) ?? defaultEmail;
final password = await _storage.read(key: _kPassword) ?? defaultPassword;
final enabled = (enabledStr ?? (defaultEnabled ? 'true' : 'false')) == 'true';
return RemoteSettings(
enabled: enabled,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
}
/// Scrive i setting nel secure storage.
Future<void> save() async {
await _storage.write(key: _kEnabled, value: enabled ? 'true' : 'false');
await _storage.write(key: _kBaseUrl, value: baseUrl);
await _storage.write(key: _kIndexPath, value: indexPath);
await _storage.write(key: _kEmail, value: email);
await _storage.write(key: _kPassword, value: password);
}
/// In DEBUG: se un valore non è ancora impostato, inizializzalo con i default.
/// NON sovrascrive valori già presenti (quindi puoi sempre entrare in Settings e cambiare).
static Future<void> debugSeedIfEmpty() async {
if (!kDebugMode) return;
Future<void> _seed(String key, String value) async {
final existing = await _storage.read(key: key);
if (existing == null) {
await _storage.write(key: key, value: value);
}
}
await _seed(_kEnabled, defaultEnabled ? 'true' : 'false');
await _seed(_kBaseUrl, defaultBaseUrl);
await _seed(_kIndexPath, defaultIndexPath);
await _seed(_kEmail, defaultEmail);
await _seed(_kPassword, defaultPassword);
}
}

View file

@ -1,94 +0,0 @@
import 'package:flutter/material.dart';
import 'remote_settings.dart';
class RemoteSettingsPage extends StatefulWidget {
const RemoteSettingsPage({super.key});
@override
State<RemoteSettingsPage> createState() => _RemoteSettingsPageState();
}
class _RemoteSettingsPageState extends State<RemoteSettingsPage> {
final _form = GlobalKey<FormState>();
bool _enabled = RemoteSettings.defaultEnabled;
final _baseUrl = TextEditingController(text: RemoteSettings.defaultBaseUrl);
final _indexPath = TextEditingController(text: RemoteSettings.defaultIndexPath);
final _email = TextEditingController();
final _password = TextEditingController();
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final s = await RemoteSettings.load();
setState(() {
_enabled = s.enabled;
_baseUrl.text = s.baseUrl;
_indexPath.text = s.indexPath;
_email.text = s.email;
_password.text = s.password;
});
}
Future<void> _save() async {
if (!_form.currentState!.validate()) return;
final s = RemoteSettings(
enabled: _enabled,
baseUrl: _baseUrl.text.trim(),
indexPath: _indexPath.text.trim(),
email: _email.text.trim(),
password: _password.text,
);
await s.save();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Impostazioni remote salvate')));
Navigator.of(context).maybePop();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Remote Settings')),
body: Form(
key: _form,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Abilita sync remoto'),
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
),
TextFormField(
controller: _baseUrl,
decoration: const InputDecoration(labelText: 'Base URL (es. https://server.tld)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _indexPath,
decoration: const InputDecoration(labelText: 'Index path (es. photos/)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _email,
decoration: const InputDecoration(labelText: 'User/Email'),
),
TextFormField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: const Text('Salva'),
),
],
),
),
);
}
}

View file

@ -1,647 +0,0 @@
// lib/remote/remote_test_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sqflite/sqflite.dart';
// Integrazione impostazioni & auth remota (Fase 1)
import 'remote_settings.dart';
import 'auth_client.dart';
import 'url_utils.dart';
enum _RemoteFilter { all, visibleOnly, trashedOnly }
class RemoteTestPage extends StatefulWidget {
final Database db;
/// Base URL preferita (es. https://prova.patachina.it).
/// Se non la passi o è vuota, verrà usata quella in RemoteSettings.
final String? baseUrl;
const RemoteTestPage({
super.key,
required this.db,
this.baseUrl,
});
@override
State<RemoteTestPage> createState() => _RemoteTestPageState();
}
class _RemoteTestPageState extends State<RemoteTestPage> {
Future<List<_RemoteRow>>? _future;
String _baseUrl = '';
Map<String, String>? _authHeaders;
bool _navigating = false; // debounce del tap
_RemoteFilter _filter = _RemoteFilter.all;
// contatori diagnostici
int _countAll = 0;
int _countVisible = 0; // trashed=0
int _countTrashed = 0; // trashed=1
@override
void initState() {
super.initState();
_init(); // prepara baseUrl + header auth (se necessari), poi carica i dati
}
Future<void> _init() async {
// 1) Base URL: parametro > settings
final s = await RemoteSettings.load();
final candidate = (widget.baseUrl ?? '').trim();
_baseUrl = candidate.isNotEmpty ? candidate : s.baseUrl.trim();
// 2) Header Authorization (opzionale)
_authHeaders = null;
try {
if (_baseUrl.isNotEmpty && (s.email.isNotEmpty || s.password.isNotEmpty)) {
final auth = RemoteAuth(baseUrl: _baseUrl, email: s.email, password: s.password);
final token = await auth.login();
_authHeaders = {'Authorization': 'Bearer $token'};
}
} catch (_) {
// In debug non bloccare la pagina se il login immagini fallisce
_authHeaders = null;
}
// 3) Carica contatori e lista
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
}
Future<void> _refreshCounters() async {
// Totale remoti (origin=1), visibili e cestinati
final all = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1",
);
final vis = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=0",
);
final tra = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=1",
);
_countAll = (all.first['c'] as int?) ?? 0;
_countVisible = (vis.first['c'] as int?) ?? 0;
_countTrashed = (tra.first['c'] as int?) ?? 0;
}
Future<List<_RemoteRow>> _load() async {
// Filtro WHERE in base al toggle
String extraWhere = '';
switch (_filter) {
case _RemoteFilter.visibleOnly:
extraWhere = ' AND trashed=0';
break;
case _RemoteFilter.trashedOnly:
extraWhere = ' AND trashed=1';
break;
case _RemoteFilter.all:
default:
extraWhere = '';
}
// Prende le prime 300 entry remote (includiamo il mime e il remoteId)
final rows = await widget.db.rawQuery(
'SELECT id, remoteId, title, remotePath, remoteThumb2, sourceMimeType, trashed '
'FROM entry WHERE origin=1$extraWhere '
'ORDER BY id DESC LIMIT 300',
);
return rows.map((r) {
return _RemoteRow(
id: r['id'] as int,
remoteId: (r['remoteId'] as String?) ?? '',
title: (r['title'] as String?) ?? '',
remotePath: r['remotePath'] as String?,
remoteThumb2: r['remoteThumb2'] as String?,
mime: r['sourceMimeType'] as String?,
trashed: (r['trashed'] as int?) ?? 0,
);
}).toList();
}
// Costruzione robusta dellURL assoluto:
// - se già assoluto ritorna comè
// - se relativo risolve contro _baseUrl (accetta con/senza '/')
String _absUrl(String? relativePath) {
if (relativePath == null || relativePath.isEmpty) return '';
final p = relativePath.trim();
// URL già assoluto
if (p.startsWith('http://') || p.startsWith('https://')) return p;
if (_baseUrl.isEmpty) return '';
try {
final base = Uri.parse(_baseUrl.endsWith('/') ? _baseUrl : '$_baseUrl/');
// normalizza: se inizia con '/', togliamo per usare resolve coerente
final rel = p.startsWith('/') ? p.substring(1) : p;
final resolved = base.resolve(rel);
return resolved.toString();
} catch (_) {
return '';
}
}
bool _isVideo(String? mime, String? path) {
final m = (mime ?? '').toLowerCase();
final p = (path ?? '').toLowerCase();
return m.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Future<void> _onRefresh() async {
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
await _future;
}
Future<void> _diagnosticaDb() async {
try {
final dup = await widget.db.rawQuery('''
SELECT remoteId, COUNT(*) AS cnt
FROM entry
WHERE origin=1 AND remoteId IS NOT NULL
GROUP BY remoteId
HAVING cnt > 1
''');
final vis = await widget.db.rawQuery('''
SELECT COUNT(*) AS visible_remotes
FROM entry
WHERE origin=1 AND trashed=0
''');
final idx = await widget.db.rawQuery("PRAGMA index_list('entry')");
if (!mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Diagnostica DB', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('Duplicati per remoteId:\n${dup.isEmpty ? "nessuno" : dup.map((e)=>e.toString()).join('\n')}'),
const SizedBox(height: 12),
Text('Remoti visibili in Aves (trashed=0): ${vis.first.values.first}'),
const SizedBox(height: 12),
Text('Indici su entry:\n${idx.map((e)=>e.toString()).join('\n')}'),
],
),
),
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Diagnostica DB fallita: $e')),
);
}
}
/// 🔧 Pulisce duplicati per `remotePath` (tiene MAX(id)) e righe senza `remoteId`.
Future<void> _pulisciDuplicatiPath() async {
try {
final delNoId = await widget.db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
final delByPath = await widget.db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
await _onRefresh();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia completata: noId=$delNoId, dupPath=$delByPath')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia fallita: $e')),
);
}
}
Future<void> _nascondiRemotiInCollection() async {
try {
final changed = await widget.db.rawUpdate('''
UPDATE entry SET trashed=1
WHERE origin=1 AND trashed=0
''');
if (!mounted) return;
await _onRefresh();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Remoti nascosti dalla Collection: $changed')),
);
} on DatabaseException catch (e) {
final msg = e.toString();
if (!mounted) return;
// Probabile connessione R/O: istruisci a riaprire il DB in R/W
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
'UPDATE fallito (DB in sola lettura?): $msg\n'
'Apri il DB in R/W in HomePage._openRemoteTestPage (no readOnly).',
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore UPDATE: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final ready = (_baseUrl.isNotEmpty && _future != null);
return Scaffold(
appBar: AppBar(
title: const Text('[DEBUG] Remote Test'),
actions: [
IconButton(
icon: const Icon(Icons.bug_report_outlined),
tooltip: 'Diagnostica DB',
onPressed: _diagnosticaDb,
),
IconButton(
icon: const Icon(Icons.cleaning_services_outlined),
tooltip: 'Pulisci duplicati (path)',
onPressed: _pulisciDuplicatiPath,
),
IconButton(
icon: const Icon(Icons.visibility_off_outlined),
tooltip: 'Nascondi remoti in Collection',
onPressed: _nascondiRemotiInCollection,
),
],
),
body: !ready
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// Header contatori + filtro
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 8,
runSpacing: -6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(label: Text('Tot: $_countAll')),
Chip(label: Text('Visibili: $_countVisible')),
Chip(label: Text('Cestinati: $_countTrashed')),
],
),
),
const SizedBox(width: 8),
SegmentedButton<_RemoteFilter>(
segments: const [
ButtonSegment(value: _RemoteFilter.all, label: Text('Tutti')),
ButtonSegment(value: _RemoteFilter.visibleOnly, label: Text('Visibili')),
ButtonSegment(value: _RemoteFilter.trashedOnly, label: Text('Cestinati')),
],
selected: {_filter},
onSelectionChanged: (sel) async {
setState(() => _filter = sel.first);
await _onRefresh();
},
),
],
),
),
const Divider(height: 1),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: FutureBuilder<List<_RemoteRow>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: Center(child: Text('Errore: ${snap.error}')),
),
);
}
final items = snap.data ?? const <_RemoteRow>[];
if (items.isEmpty) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: const Center(child: Text('Nessuna entry remota (origin=1)')),
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, mainAxisSpacing: 4, crossAxisSpacing: 4,
),
itemCount: items.length,
itemBuilder: (context, i) {
final it = items[i];
final isVideo = _isVideo(it.mime, it.remotePath);
final thumbUrl = _absUrl(it.remoteThumb2);
final fullUrl = _absUrl(it.remotePath);
final hasThumb = thumbUrl.isNotEmpty;
final hasFull = fullUrl.isNotEmpty;
final heroTag = 'remote_${it.id}';
return GestureDetector(
onLongPress: () async {
if (!context.mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${it.id} remoteId: ${it.remoteId} trashed: ${it.trashed}'),
const SizedBox(height: 8),
Text('MIME: ${it.mime}'),
const Divider(),
SelectableText('FULL URL:\n$fullUrl'),
const SizedBox(height: 8),
SelectableText('THUMB URL:\n$thumbUrl'),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
ElevatedButton.icon(
onPressed: hasFull
? () async {
await Clipboard.setData(ClipboardData(text: fullUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('FULL URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy),
label: const Text('Copia FULL'),
),
ElevatedButton.icon(
onPressed: hasThumb
? () async {
await Clipboard.setData(ClipboardData(text: thumbUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('THUMB URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy_all),
label: const Text('Copia THUMB'),
),
],
),
],
),
),
),
),
);
},
onTap: () async {
if (_navigating) return; // debounce
_navigating = true;
try {
if (isVideo) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video remoto: anteprima full non disponibile (thumb richiesta).'),
duration: Duration(seconds: 2),
),
);
return;
}
if (!hasFull) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL non valido')),
);
return;
}
await Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (_, __, ___) => _RemoteFullPage(
title: it.title,
url: fullUrl,
headers: _authHeaders,
heroTag: heroTag, // pairing Hero
),
transitionDuration: const Duration(milliseconds: 220),
),
);
} finally {
_navigating = false;
}
},
child: Hero(
tag: heroTag, // pairing Hero
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all(color: Colors.black12)),
child: Stack(
fit: StackFit.expand,
children: [
_buildGridTile(isVideo, thumbUrl, fullUrl),
// Informazioni utili per capire cosa stiamo vedendo
Positioned(
left: 2,
bottom: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
color: Colors.black54,
child: Text(
'id:${it.id} rid:${it.remoteId}${it.trashed==1 ? " (T)" : ""}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
Positioned(
right: 2,
top: 2,
child: Wrap(
spacing: 4,
children: [
if (hasFull)
const _MiniBadge(label: 'URL')
else
const _MiniBadge(label: 'NOURL', color: Colors.red),
if (hasThumb)
const _MiniBadge(label: 'THUMB')
else
const _MiniBadge(label: 'NOTH', color: Colors.orange),
],
),
),
],
),
),
),
);
},
);
},
),
),
),
],
),
);
}
Widget _buildGridTile(bool isVideo, String thumbUrl, String fullUrl) {
if (isVideo) {
// Per i video: NON usiamo Image.network(fullUrl).
// Usiamo la thumb se c'è, altrimenti placeholder con icona "play".
final base = thumbUrl.isEmpty
? const ColoredBox(color: Colors.black12)
: Image.network(
thumbUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
return Stack(
fit: StackFit.expand,
children: [
base,
const Align(
alignment: Alignment.center,
child: Icon(Icons.play_circle_fill, color: Colors.white70, size: 48),
),
],
);
}
// Per le immagini: se non c'è thumb, posso usare direttamente l'URL full.
final displayUrl = thumbUrl.isEmpty ? fullUrl : thumbUrl;
if (displayUrl.isEmpty) {
return const ColoredBox(color: Colors.black12);
}
return Image.network(
displayUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
}
}
class _RemoteRow {
final int id;
final String remoteId;
final String title;
final String? remotePath;
final String? remoteThumb2;
final String? mime;
final int trashed;
_RemoteRow({
required this.id,
required this.remoteId,
required this.title,
this.remotePath,
this.remoteThumb2,
this.mime,
required this.trashed,
});
}
class _MiniBadge extends StatelessWidget {
final String label;
final Color color;
const _MiniBadge({super.key, required this.label, this.color = Colors.black54});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(3),
),
child: Text(label, style: const TextStyle(fontSize: 9, color: Colors.white)),
);
}
}
class _RemoteFullPage extends StatelessWidget {
final String title;
final String url;
final Map<String, String>? headers;
final String heroTag; // pairing Hero
const _RemoteFullPage({
super.key,
required this.title,
required this.url,
required this.heroTag,
this.headers,
});
@override
Widget build(BuildContext context) {
final body = url.isEmpty
? const Text('URL non valido')
: Hero(
tag: heroTag, // pairing con la griglia
child: InteractiveViewer(
maxScale: 5,
child: Image.network(
url,
fit: BoxFit.contain,
headers: headers, // Authorization se il server lo richiede
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 64),
),
),
);
return Scaffold(
appBar: AppBar(title: Text(title.isEmpty ? 'Remote' : title)),
body: Center(child: body),
);
}
}

View file

@ -1,194 +0,0 @@
// lib/remote/run_remote_sync.dart
//
// Esegue un ciclo di sincronizzazione "pull":
// 1) legge le impostazioni (server, path, user, password) da RemoteSettings
// 2) login Bearer token
// 3) GET dell'indice JSON (array di oggetti foto)
// 4) upsert nel DB 'entry' (e 'address' se presente) tramite RemoteRepository
//
// NOTE:
// - La versione "managed" (runRemoteSyncOnceManaged) apre/chiude il DB ed evita run concorrenti.
// - La versione "plain" (runRemoteSyncOnce) usa un Database già aperto (compatibilità).
// - PRAGMA per concorrenza (WAL, busy_timeout, ...).
// - Non logghiamo contenuti sensibili (password/token/body completi).
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
import 'remote_client.dart';
import 'remote_repository.dart';
// === Guardia anti-concorrenza (single-flight) per la run "managed" ===
bool _remoteSyncRunning = false;
/// Helper: retry esponenziale breve per SQLITE_BUSY.
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
final msg = e.toString();
final isBusy = msg.contains('SQLITE_BUSY') || msg.contains('database is locked');
if (!isBusy || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
/// Versione "managed":
/// - impedisce run concorrenti
/// - apre/chiude da sola la connessione a `metadata.db` (istanza indipendente)
/// - imposta PRAGMA per concorrenza
/// - accetta override opzionali (utile in test)
Future<void> runRemoteSyncOnceManaged({
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
if (_remoteSyncRunning) {
// ignore: avoid_print
print('[remote-sync] already running, skip');
return;
}
_remoteSyncRunning = true;
Database? db;
try {
final dbDir = await getDatabasesPath();
final dbPath = p.join(dbDir, 'metadata.db');
db = await openDatabase(
dbPath,
singleInstance: false, // connessione indipendente (non chiude lhandle di Aves)
onConfigure: (db) async {
try {
// Alcuni PRAGMA ritornano valori usare SEMPRE rawQuery.
await db.rawQuery('PRAGMA journal_mode=WAL');
await db.rawQuery('PRAGMA synchronous=NORMAL');
await db.rawQuery('PRAGMA busy_timeout=3000');
await db.rawQuery('PRAGMA wal_autocheckpoint=1000');
await db.rawQuery('PRAGMA foreign_keys=ON');
// (Opzionale) verifica del mode corrente
final jm = await db.rawQuery('PRAGMA journal_mode');
final mode = jm.isNotEmpty ? jm.first.values.first : null;
// ignore: avoid_print
print('[remote-sync] journal_mode=$mode'); // atteso: wal
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][WARN] PRAGMA setup failed: $e\n$st');
// Non rilanciare: in estremo, continueremo con journaling di default
}
},
);
await runRemoteSyncOnce(
db: db,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
} finally {
try {
await db?.close();
} catch (_) {
// In caso di close doppio/già chiuso, ignoro.
}
_remoteSyncRunning = false;
}
}
/// Versione "plain":
/// Esegue login, scarica /photos e fa upsert nel DB usando una connessione
/// SQLite **già aperta** (non viene chiusa qui).
///
/// Gli optional [baseUrl], [indexPath], [email], [password] permettono override
/// delle impostazioni salvate in `RemoteSettings` (comodo per test / debug).
Future<void> runRemoteSyncOnce({
required Database db,
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
try {
// 1) Carica impostazioni sicure (secure storage)
final s = await RemoteSettings.load();
final bUrl = (baseUrl ?? s.baseUrl).trim();
final ip = (indexPath ?? s.indexPath).trim();
final em = (email ?? s.email).trim();
final pw = (password ?? s.password);
if (bUrl.isEmpty || ip.isEmpty) {
throw StateError('Impostazioni remote incomplete: baseUrl/indexPath mancanti');
}
// 2) Autenticazione (Bearer)
final auth = RemoteAuth(baseUrl: bUrl, email: em, password: pw);
await auth.login(); // Se necessario, RemoteJsonClient può riloggare su 401
// 3) Client JSON (segue anche redirect 301/302/307/308)
final client = RemoteJsonClient(bUrl, ip, auth: auth);
// 4) Scarica lelenco di elementi remoti (array top-level)
final items = await client.fetchAll();
// 5) Upsert nel DB (con retry se incappiamo in SQLITE_BUSY)
final repo = RemoteRepository(db);
await _withRetryBusy(() => repo.upsertAll(items));
// 5.b) Impedisci futuri duplicati e ripulisci quelli già presenti
await repo.ensureUniqueRemoteId();
final removed = await repo.deduplicateRemotes();
// 5.c) Paracadute: assicura che i remoti NON siano mostrati nella Collection Aves
//await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;');
// 5.d) **CLEANUP LEGACY**: elimina righe remote "orfane" o doppioni su remotePath
// - Righe senza remoteId (NULL o vuoto): non deduplicabili via UNIQUE vanno rimosse
final purgedNoId = await db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
// - Doppioni per remotePath: tieni solo la riga con id MAX
// (copre i casi in cui in passato siano state create due righe per lo stesso path)
final purgedByPath = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
// ignore: avoid_print
print('[remote-sync] cleanup: removed dup(remoteId)=$removed, purged(noId)=$purgedNoId, purged(byPath)=$purgedByPath');
// 6) Log sintetico
int? c;
try {
c = await repo.countRemote();
} catch (_) {
c = null;
}
// ignore: avoid_print
if (c == null) {
print('[remote-sync] import completato (conteggio non disponibile)');
} else {
print('[remote-sync] importati remoti: $c (base=$bUrl, index=$ip)');
}
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][ERROR] $e\n$st');
rethrow;
}
}

View file

@ -1,7 +0,0 @@
// lib/remote/url_utils.dart
Uri buildAbsoluteUri(String baseUrl, String relativePath) {
final base = Uri.parse(baseUrl.endsWith('/') ? baseUrl : '$baseUrl/');
final cleaned = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
final segments = cleaned.split('/').where((s) => s.isNotEmpty).toList();
return base.replace(pathSegments: [...base.pathSegments, ...segments]);
}

View file

@ -0,0 +1,27 @@
// lib/remote/remote_db_uris.dart
import 'dart:convert';
class RemoteDbUris {
// Schema personalizzato che non attiva ContentResolver.
static const _scheme = 'aves-remote';
/// Costruisce un URI fittizio univoco e stabile per un elemento remoto.
/// - Se hai un remoteId (hash/uuid) usa quello (preferibile).
/// - In alternativa, codifica il remotePath.
static String make({String? remoteId, String? remotePath}) {
if (remoteId != null && remoteId.trim().isNotEmpty) {
// Esempio: aves-remote://rid/<remoteId>
return '$_scheme://rid/${Uri.encodeComponent(remoteId.trim())}';
}
if (remotePath != null && remotePath.trim().isNotEmpty) {
// Esempio: aves-remote://path/<base64url(remotePath)>
final b64 = base64Url.encode(utf8.encode(remotePath.trim()));
return '$_scheme://path/$b64';
}
// Estremo fallback (pochissimo probabile)
return '$_scheme://anon/${DateTime.now().microsecondsSinceEpoch}';
}
/// Riconosce se un uri è fittizio remoto
static bool isSynthetic(String? uri) => uri != null && uri.startsWith('$_scheme://');
}

View file

@ -1,19 +1,10 @@
// lib/remote/remote_gallery_bridge.dart
import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/model/entry/entry.dart';
class RemoteGalleryBridge {
static Future<Set<AvesEntry>> loadRemoteEntries() async {
final remotes = await localMediaDb.loadEntries(origin: 1); // usa API esistente
return remotes.where((e) => e.trashed == 0).toSet();
final allRemotes = await localMediaDb.loadEntries(origin: 1);
return allRemotes.where((e) => e.trashed == 0).toSet();
}
static List<AvesEntry> mergeWithLocal(List<AvesEntry> locals, Set<AvesEntry> remotes) {
final ids = {...locals.map((e) => e.id)};
final merged = <AvesEntry>[...locals];
for (final r in remotes) {
if (!ids.contains(r.id)) merged.add(r);
}
return merged;
}
}
}

View file

@ -1,26 +1,104 @@
// lib/remote/remote_http.dart
import 'dart:developer' as dev;
import 'package:flutter/services.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
/// Helper HTTP per risorse remote (URL + headers di autenticazione).
class RemoteHttp {
static RemoteAuth? _auth;
static String? _base;
static Future<void> init() async {
final s = await RemoteSettings.load();
_base = s.baseUrl.trim().isEmpty ? null : s.baseUrl.trim();
_auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password);
// Cache dell'ultimo set di header (mai null: {} se non autenticato)
static Map<String, String> _cachedHeaders = const {};
static Future<void>? _initFuture; // evita init concorrenti
/// Inizializza auth e base URL dai settings sicuri (robusto: non lancia).
static Future<void> init() {
_initFuture ??= _doInit();
return _initFuture!;
}
static Future<void> _doInit() async {
try {
final s = await RemoteSettings.load(); // safe lato storage
final base = s.baseUrl.trim();
_base = base.isEmpty ? null : base;
_auth = RemoteAuth(baseUrl: s.baseUrl, email: s.email, password: s.password);
dev.log('[RemoteHttp] init: base=$_base, email=${s.email.isNotEmpty ? '***' : '(none)'}', name: 'RemoteHttp');
} catch (e, st) {
dev.log('[RemoteHttp] init ERROR: $e', name: 'RemoteHttp', error: e, stackTrace: st);
_auth = null;
_base = null;
}
}
/// Headers correnti (login ondemand). Non lancia; ritorna {} se non disponibili.
static Future<Map<String, String>> headers() async {
if (_auth == null) await init();
return await _auth!.authHeaders(); // login on-demand
if (_auth == null) {
await init();
if (_auth == null) {
_cachedHeaders = const {};
dev.log('[RemoteHttp] headers: init failed → NO TOKEN', name: 'RemoteHttp');
return _cachedHeaders;
}
}
try {
final h = await _auth!.authHeaders(); // login on-demand
_cachedHeaders = Map<String, String>.from(h);
if ((_cachedHeaders['Authorization']?.isEmpty ?? true)) {
dev.log('[RemoteHttp] headers: NO Authorization', name: 'RemoteHttp');
} else {
dev.log('[RemoteHttp] headers: Authorization: Bearer ***', name: 'RemoteHttp');
}
return _cachedHeaders;
} on PlatformException catch (e, st) {
// Tipico: secure storage/keystore corrotto non rompiamo la UI
dev.log('[RemoteHttp] headers PlatformException: $e → returning {}',
name: 'RemoteHttp', error: e, stackTrace: st);
_cachedHeaders = const {};
return _cachedHeaders;
} catch (e, st) {
dev.log('[RemoteHttp] headers ERROR: $e → returning {}',
name: 'RemoteHttp', error: e, stackTrace: st);
_cachedHeaders = const {};
return _cachedHeaders;
}
}
/// Ultimi header noti (sincrono). Mai null; {} se non autenticato.
static Map<String, String> peekHeaders() => _cachedHeaders;
/// Converte path relativo in assoluto. Se è già http/https lo ritorna comè.
static String absUrl(String? relativePath) {
if (_base == null || _base!.isEmpty || relativePath == null || relativePath.isEmpty) return '';
if (relativePath == null || relativePath.isEmpty) return '';
final lp = relativePath.trim().toLowerCase();
if (lp.startsWith('http://') || lp.startsWith('https://')) {
return relativePath;
}
if (_base == null || _base!.isEmpty) return '';
final b = _base!.endsWith('/') ? _base! : '${_base!}/';
final rel = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
return '$b$rel';
}
}
/// Facoltativo: warmup allavvio per scaldare gli header.
static Future<void> warmUp() async {
await init();
try {
await headers();
} catch (_) {
// già loggato in headers()
}
}
/// Rilegge i settings a runtime (es. utente cambia base/email/password).
static Future<void> refreshFromSettings() async {
_initFuture = null;
await init();
_cachedHeaders = const {}; // saranno ricaricati al primo uso
}
}

View file

@ -5,18 +5,15 @@ import 'package:aves/model/entry/entry.dart';
class RemoteImageTile extends StatelessWidget {
final AvesEntry entry;
const RemoteImageTile({super.key, required this.entry});
@override
Widget build(BuildContext context) {
// Usa SOLO campi remoti, mai entry.path
final rel = entry.remoteThumb2 ?? entry.remoteThumb1 ?? entry.remotePath;
if (rel == null || rel.isEmpty) {
return const ColoredBox(color: Colors.black12);
}
final url = RemoteHttp.absUrl(rel);
return FutureBuilder<Map<String, String>>(
@ -25,13 +22,11 @@ class RemoteImageTile extends StatelessWidget {
if (snap.connectionState != ConnectionState.done) {
return const ColoredBox(color: Colors.black12);
}
final hdrs = snap.data ?? const {};
return Image.network(
url,
fit: BoxFit.cover,
headers: hdrs,
headers: hdrs.isEmpty ? null : hdrs,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image),
);
},

View file

@ -9,9 +9,12 @@ class RemoteImageTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final rel = entry.remoteThumb2 ?? entry.remotePath ?? entry.path;
// Usa SOLO campi remoti, mai entry.path
final rel = entry.remoteThumb2 ?? entry.remoteThumb1 ?? entry.remotePath;
if (rel == null || rel.isEmpty) {
return const ColoredBox(color: Colors.black12);
}
final url = RemoteHttp.absUrl(rel);
if (url.isEmpty) return const ColoredBox(color: Colors.black12);
return FutureBuilder<Map<String, String>>(
future: RemoteHttp.headers(),
@ -23,10 +26,10 @@ class RemoteImageTile extends StatelessWidget {
return Image.network(
url,
fit: BoxFit.cover,
headers: hdrs,
headers: hdrs.isEmpty ? null : hdrs,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image),
);
},
);
}
}
}

View file

@ -8,6 +8,7 @@ class RemotePhotoItem {
final String? thub1, thub2;
final String? mimeType;
final int? width, height, sizeBytes;
final int? rotation;
final DateTime? takenAtUtc;
final double? lat, lng, alt;
final String? dataExifLegacy;
@ -25,6 +26,7 @@ class RemotePhotoItem {
this.mimeType,
this.width,
this.height,
this.rotation,
this.sizeBytes,
this.takenAtUtc,
this.lat,
@ -75,6 +77,7 @@ class RemotePhotoItem {
mimeType: j['mime_type']?.toString(),
width: (j['width'] as num?)?.toInt(),
height: (j['height'] as num?)?.toInt(),
rotation: (j['rotation'] as num?)?.toInt(),
sizeBytes: (j['size_bytes'] as num?)?.toInt(),
takenAtUtc: _tryParseIsoUtc(j['taken_at']),
dataExifLegacy: j['data']?.toString(),

View file

@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart' show debugPrint;
import 'package:sqflite/sqflite.dart';
import 'remote_models.dart';
import 'remote_db_uris.dart'; // <-- helper per URI fittizi aves-remote://...
class RemoteRepository {
final Database db;
@ -39,13 +40,34 @@ class RemoteRepository {
}
}
/// Assicura che tutte le entry remote abbiano un uri costruito da remoteId.
Future<void> ensureRemoteUris(DatabaseExecutor dbExec) async {
try {
await dbExec.execute('''
UPDATE entry
SET uri = 'remote://' || remoteId
WHERE origin = 1
AND remoteId IS NOT NULL
AND remoteId != ''
AND (uri IS NULL OR uri = '' OR uri NOT LIKE 'remote://%');
''');
debugPrint('[RemoteRepository] ensureRemoteUris: migration applied');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureRemoteUris error: $e\n$st');
}
}
/// Assicura che le colonne GPS e alcune colonne "remote*" esistano nella tabella `entry`.
Future<void> _ensureEntryColumns(DatabaseExecutor dbExec) async {
await _ensureColumns(dbExec, table: 'entry', columnsAndTypes: const {
// Core (alcune basi legacy potrebbero non averle ancora)
'uri': 'TEXT',
// GPS
'latitude': 'REAL',
'longitude': 'REAL',
'altitude': 'REAL',
// Campi remoti
'remoteId': 'TEXT',
'remotePath': 'TEXT',
@ -54,7 +76,9 @@ class RemoteRepository {
'origin': 'INTEGER',
'provider': 'TEXT',
'trashed': 'INTEGER',
'remoteRotation': 'INTEGER',
});
// Indice "normale" per velocizzare il lookup su remoteId
try {
await dbExec.execute(
@ -130,39 +154,72 @@ class RemoteRepository {
p.endsWith('.webm');
}
Map<String, Object?> _buildEntryRow(RemotePhotoItem it, {int? existingId}) {
// Salviamo ciò che arriva (il server ora emette già il path canonico con /original/)
return <String, Object?>{
'id': existingId,
'contentId': null,
'uri': 'remote://${it.id}',
'path': it.path,
'sourceMimeType': it.mimeType,
'width': it.width,
'height': it.height,
'sourceRotationDegrees': null,
'sizeBytes': it.sizeBytes,
'title': it.name,
'dateAddedSecs': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'dateModifiedMillis': null,
'sourceDateTakenMillis': it.takenAtUtc?.millisecondsSinceEpoch,
'durationMillis': it.durationMillis,
// REMOTI VISIBILI
'trashed': 0,
'origin': 1,
'provider': 'json@patachina',
// GPS (possono essere null)
'latitude': it.lat,
'longitude': it.lng,
'altitude': it.alt,
// campi remoti
'remoteId': it.id,
'remotePath': it.path,
'remoteThumb1': it.thub1,
'remoteThumb2': it.thub2,
};
Map<String, Object?> _buildEntryRow(RemotePhotoItem it, {int? existingId}) {
// ============================================================
// REMARK ORIGINALE (da ripristinare quando avrai ImageProvider)
final syntheticUri = RemoteDbUris.make(remoteId: it.id, remotePath: it.path);
// ============================================================
// TEMPORARY FIX: usare URL HTTP basato su thub2
//final syntheticUri = 'https://prova.patachina.it/${it.thub2}';
//final syntheticUri = 'https://picsum.photos/400';
int _makeContentId() {
final base = (it.id.isNotEmpty ? it.id : it.path);
final h = base.hashCode & 0x7fffffff;
return 1_000_000_000 + (h % 900_000_000);
}
final nowMs = DateTime.now().millisecondsSinceEpoch;
final dateModMs = it.takenAtUtc?.millisecondsSinceEpoch ?? nowMs;
return <String, Object?>{
'id': existingId,
'contentId': _makeContentId(),
// URI HTTP temporaneo
'uri': syntheticUri,
// MIME sempre valorizzato
'path': it.path,
'sourceMimeType': it.mimeType ?? 'image/jpeg',
// width/height sempre valorizzati
'width': it.width ?? 0,
'height': it.height ?? 0,
// rotation sempre valorizzata
'sourceRotationDegrees': it.rotation ?? 0,
'sizeBytes': it.sizeBytes,
'title': it.name,
'dateAddedSecs': nowMs ~/ 1000,
'dateModifiedMillis': dateModMs,
'sourceDateTakenMillis': it.takenAtUtc?.millisecondsSinceEpoch,
'durationMillis': it.durationMillis,
'trashed': 0,
'origin': 1,
'provider': 'json@patachina',
'latitude': it.lat,
'longitude': it.lng,
'altitude': it.alt,
'remoteId': it.id,
'remotePath': it.path,
'remoteThumb1': it.thub1,
'remoteThumb2': it.thub2,
'remoteRotation': it.rotation ?? 0,
// remoteWidth/remoteHeight sempre valorizzati
'remoteWidth': it.width ?? 0,
'remoteHeight': it.height ?? 0,
};
}
Map<String, Object?> _buildAddressRow(int newId, RemoteLocation location) {
return <String, Object?>{
'id': newId,
@ -391,12 +448,111 @@ class RemoteRepository {
}
}
/// Helper combinato: pulizia + indici.
// =========================
// Backfill URI fittizi per remoti legacy
// =========================
/// Imposta un URI fittizio `aves-remote://...` per tutte le righe remote
/// con `uri` NULL/vuoto. Prima prova a usare `remoteId` (SQL puro),
/// poi completa i rimanenti (senza remoteId) in un loop Dart usando `remotePath`.
Future<void> backfillRemoteUris() async {
// 1) Backfill via SQL per chi ha remoteId (più veloce)
try {
final updated = await db.rawUpdate(
"UPDATE entry "
"SET uri = 'aves-remote://rid/' || replace(remoteId, ' ', '') "
"WHERE origin=1 AND (uri IS NULL OR trim(uri)='') AND remoteId IS NOT NULL",
);
debugPrint('[RemoteRepository] backfill URIs via SQL (remoteId) updated=$updated');
} catch (e, st) {
debugPrint('[RemoteRepository] backfill URIs (SQL) error: $e\n$st');
}
// 2) Loop Dart per i (pochi) rimanenti senza remoteId ma con remotePath
try {
final rows = await db.rawQuery(
"SELECT id, remotePath FROM entry "
"WHERE origin=1 AND (uri IS NULL OR trim(uri)='') "
"AND (remoteId IS NULL OR trim(remoteId)='') "
"AND remotePath IS NOT NULL"
);
if (rows.isNotEmpty) {
for (final r in rows) {
final id = (r['id'] as num).toInt();
final rp = (r['remotePath'] as String?) ?? '';
final synthetic = RemoteDbUris.make(remotePath: rp);
await db.update(
'entry',
{'uri': synthetic},
where: 'id=?',
whereArgs: [id],
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
debugPrint('[RemoteRepository] backfill URIs via Dart (remotePath) updated=${rows.length}');
}
} catch (e, st) {
debugPrint('[RemoteRepository] backfill URIs (Dart) error: $e\n$st');
}
}
// =========================
// Backfill ESSENZIALI (contentId, dateModifiedMillis) per remoti legacy
// =========================
Future<void> backfillRemoteEssentials() async {
// 1) backfill contentId sintetico per remoti con contentId NULL/<=0
try {
final rows = await db.rawQuery(
"SELECT id, remoteId, remotePath FROM entry "
"WHERE origin=1 AND (contentId IS NULL OR contentId<=0)"
);
if (rows.isNotEmpty) {
for (final r in rows) {
final id = (r['id'] as num).toInt();
final rid = (r['remoteId'] as String?) ?? '';
final rpath = (r['remotePath'] as String?) ?? '';
final base = rid.isNotEmpty ? rid : rpath;
final h = base.hashCode & 0x7fffffff; // positivo
final cid = 1_000_000_000 + (h % 900_000_000);
await db.update('entry', {'contentId': cid}, where: 'id=?', whereArgs: [id]);
}
debugPrint('[RemoteRepository] backfill contentId updated=${rows.length}');
}
} catch (e, st) {
debugPrint('[RemoteRepository] backfill contentId error: $e\n$st');
}
// 2) backfill dateModifiedMillis (usa sourceDateTakenMillis se presente, altrimenti now)
try {
final nowMs = DateTime.now().millisecondsSinceEpoch;
final updated = await db.rawUpdate(
"UPDATE entry SET dateModifiedMillis = COALESCE(sourceDateTakenMillis, ?) "
"WHERE origin=1 AND (dateModifiedMillis IS NULL OR dateModifiedMillis=0)",
[nowMs],
);
debugPrint('[RemoteRepository] backfill dateModifiedMillis updated=$updated');
} catch (e, st) {
debugPrint('[RemoteRepository] backfill dateModifiedMillis error: $e\n$st');
}
}
// =========================
// Helper combinato: pulizia + indici + backfill URI/ESSENZIALI
// =========================
Future<void> sanitizeRemotes() async {
await deduplicateRemotes();
await deduplicateByRemotePath(); // opzionale ma utile
await ensureUniqueRemoteId();
await ensureUniqueRemotePath();
// Assicura che ogni remoto abbia un uri fittizio valorizzato
await backfillRemoteUris();
// Assicura che i remoti abbiano contentId/dateModifiedMillis validi
await backfillRemoteEssentials();
}
// =========================

View file

@ -1,413 +0,0 @@
// lib/remote/remote_repository.dart
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:sqflite/sqflite.dart';
import 'remote_models.dart';
class RemoteRepository {
final Database db;
RemoteRepository(this.db);
// =========================
// Helpers PRAGMA / schema
// =========================
Future<void> _ensureColumns(
DatabaseExecutor dbExec, {
required String table,
required Map<String, String> columnsAndTypes,
}) async {
try {
final rows = await dbExec.rawQuery('PRAGMA table_info($table);');
final existing = rows.map((r) => (r['name'] as String)).toSet();
for (final entry in columnsAndTypes.entries) {
final col = entry.key;
final typ = entry.value;
if (!existing.contains(col)) {
final sql = 'ALTER TABLE $table ADD COLUMN $col $typ;';
try {
await dbExec.execute(sql);
debugPrint('[RemoteRepository] executed: $sql');
} catch (e, st) {
debugPrint('[RemoteRepository] failed to execute $sql: $e\n$st');
}
}
}
} catch (e, st) {
debugPrint('[RemoteRepository] _ensureColumns($table) error: $e\n$st');
}
}
/// Assicura che le colonne GPS e alcune colonne "remote*" esistano nella tabella `entry`.
Future<void> _ensureEntryColumns(DatabaseExecutor dbExec) async {
await _ensureColumns(dbExec, table: 'entry', columnsAndTypes: const {
// GPS
'latitude': 'REAL',
'longitude': 'REAL',
'altitude': 'REAL',
// Campi remoti
'remoteId': 'TEXT',
'remotePath': 'TEXT',
'remoteThumb1': 'TEXT',
'remoteThumb2': 'TEXT',
'origin': 'INTEGER',
'provider': 'TEXT',
'trashed': 'INTEGER',
});
// Indice "normale" per velocizzare il lookup su remoteId
try {
await dbExec.execute(
'CREATE INDEX IF NOT EXISTS idx_entry_remoteId ON entry(remoteId);',
);
} catch (e, st) {
debugPrint('[RemoteRepository] create index error: $e\n$st');
}
}
// =========================
// Retry su SQLITE_BUSY
// =========================
bool _isBusy(Object e) {
final s = e.toString();
return s.contains('SQLITE_BUSY') || s.contains('database is locked');
}
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
if (!_isBusy(e) || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
// =========================
// Normalizzazione / Canonicalizzazione
// =========================
/// Normalizza gli slash e forza lo slash iniziale.
String _normPath(String? p) {
if (p == null || p.isEmpty) return '';
var s = p.trim().replaceAll(RegExp(r'/+'), '/');
if (!s.startsWith('/')) s = '/$s';
return s;
}
/// Inserisce '/original/' dopo '/photos/<User>/' se manca, e garantisce filename coerente.
String _canonFullPath(String? rawPath, String fileName) {
var s = _normPath(rawPath);
final seg = s.split('/'); // ['', 'photos', '<User>', maybe 'original', ...]
if (seg.length >= 4 && seg[1] == 'photos' && seg[3] != 'original' && seg[3] != 'thumbs') {
seg.insert(3, 'original');
}
// forza il filename finale (se fornito)
if (fileName.isNotEmpty) {
seg[seg.length - 1] = fileName;
}
return seg.join('/');
}
// =========================
// Utilities
// =========================
bool _isVideoItem(RemotePhotoItem it) {
final mt = (it.mimeType ?? '').toLowerCase();
final p = (it.path).toLowerCase();
return mt.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Map<String, Object?> _buildEntryRow(RemotePhotoItem it, {int? existingId}) {
final canonical = _canonFullPath(it.path, it.name);
final thumb = _normPath(it.thub2);
return <String, Object?>{
'id': existingId,
'contentId': null,
'uri': null,
'path': canonical, // path interno
'sourceMimeType': it.mimeType,
'width': it.width,
'height': it.height,
'sourceRotationDegrees': null,
'sizeBytes': it.sizeBytes,
'title': it.name,
'dateAddedSecs': DateTime.now().millisecondsSinceEpoch ~/ 1000,
'dateModifiedMillis': null,
'sourceDateTakenMillis': it.takenAtUtc?.millisecondsSinceEpoch,
'durationMillis': it.durationMillis,
// 👇 REMOTI visibili nella Collection (se la tua Collection include origin=1)
'trashed': 0,
'origin': 1,
'provider': 'json@patachina',
// GPS (possono essere null)
'latitude': it.lat,
'longitude': it.lng,
'altitude': it.alt,
// campi remoti
'remoteId': it.id,
'remotePath': canonical, // <-- sempre canonico con /original/
'remoteThumb1': it.thub1,
'remoteThumb2': thumb,
};
}
Map<String, Object?> _buildAddressRow(int newId, RemoteLocation location) {
return <String, Object?>{
'id': newId,
'addressLine': location.address,
'countryCode': null,
'countryName': location.country,
'adminArea': location.region,
'locality': location.city,
};
}
// =========================
// Upsert a chunk
// =========================
/// Inserisce o aggiorna tutti gli elementi remoti.
///
/// - Assicura colonne `entry` (GPS + remote*)
/// - Canonicalizza i path (`/photos/<User>/original/...`)
/// - Lookup robusto: remoteId -> remotePath(canonico) -> remotePath(raw normalizzato)
/// - Ordina prima le immagini, poi i video
/// - In caso di errore schema su GPS, riprova senza i 3 campi GPS
Future<void> upsertAll(List<RemotePhotoItem> items, {int chunkSize = 200}) async {
debugPrint('RemoteRepository.upsertAll: items=${items.length}');
if (items.isEmpty) return;
// Garantisco lo schema una volta, poi procedo ai chunk
await _withRetryBusy(() => _ensureEntryColumns(db));
// Indici UNIQUE per prevenire futuri duplicati (id + path)
await ensureUniqueRemoteId();
await ensureUniqueRemotePath();
// Ordina: prima immagini, poi video
final images = <RemotePhotoItem>[];
final videos = <RemotePhotoItem>[];
for (final it in items) {
(_isVideoItem(it) ? videos : images).add(it);
}
final ordered = <RemotePhotoItem>[...images, ...videos];
for (var offset = 0; offset < ordered.length; offset += chunkSize) {
final end = (offset + chunkSize < ordered.length) ? offset + chunkSize : ordered.length;
final chunk = ordered.sublist(offset, end);
try {
await _withRetryBusy(() => db.transaction((txn) async {
final batch = txn.batch();
for (final it in chunk) {
// Lookup record esistente per stabilire l'ID (REPLACE mantiene la PK)
int? existingId;
// 1) prova per remoteId
try {
final existing = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remoteId = ?',
whereArgs: [it.id],
limit: 1,
);
if (existing.isNotEmpty) {
existingId = existing.first['id'] as int?;
} else {
// 2) fallback per remotePath canonico
final canonical = _canonFullPath(it.path, it.name);
final byCanon = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [canonical],
limit: 1,
);
if (byCanon.isNotEmpty) {
existingId = byCanon.first['id'] as int?;
} else {
// 3) ultimo fallback: remotePath "raw normalizzato" (senza forzare /original/)
final rawNorm = _normPath(it.path);
final byRaw = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND remotePath = ?',
whereArgs: [rawNorm],
limit: 1,
);
if (byRaw.isNotEmpty) {
existingId = byRaw.first['id'] as int?;
}
}
}
} catch (e, st) {
debugPrint('[RemoteRepository] lookup existingId failed for remoteId=${it.id}: $e\n$st');
}
// Riga completa (con path canonico)
final row = _buildEntryRow(it, existingId: existingId);
// Provo insert/replace con i campi completi (GPS inclusi)
try {
batch.insert(
'entry',
row,
conflictAlgorithm: ConflictAlgorithm.replace,
);
// Address: lo inseriamo in un secondo pass (post-commit) con PK certa
} on DatabaseException catch (e, st) {
// Se fallisce per schema GPS (colonne non create o tipo non compatibile), riprovo senza i 3 campi
debugPrint('[RemoteRepository] batch insert failed for remoteId=${it.id}: $e\n$st');
final rowNoGps = Map<String, Object?>.from(row)
..remove('latitude')
..remove('longitude')
..remove('altitude');
batch.insert(
'entry',
rowNoGps,
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
await batch.commit(noResult: true);
// Secondo pass per address, con PK certa
for (final it in chunk) {
if (it.location == null) continue;
try {
// cerco per remoteId, altrimenti per path canonico
final canonical = _canonFullPath(it.path, it.name);
final rows = await txn.query(
'entry',
columns: ['id'],
where: 'origin=1 AND (remoteId = ? OR remotePath = ?)',
whereArgs: [it.id, canonical],
limit: 1,
);
if (rows.isEmpty) continue;
final newId = rows.first['id'] as int;
final addr = _buildAddressRow(newId, it.location!);
await txn.insert(
'address',
addr,
conflictAlgorithm: ConflictAlgorithm.replace,
);
} catch (e, st) {
debugPrint('[RemoteRepository] insert address failed for remoteId=${it.id}: $e\n$st');
}
}
}));
} catch (e, st) {
debugPrint('[RemoteRepository] upsert chunk ${offset}..${end - 1} ERROR: $e\n$st');
rethrow;
}
}
}
// =========================
// Unicità & deduplica
// =========================
/// Crea un indice UNICO su `remoteId` limitato alle righe remote (origin=1).
Future<void> ensureUniqueRemoteId() async {
try {
await db.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_entry_remote_remoteId '
'ON entry(remoteId) WHERE origin=1',
);
debugPrint('[RemoteRepository] ensured UNIQUE index on entry(remoteId) for origin=1');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureUniqueRemoteId error: $e\n$st');
}
}
/// Crea un indice UNICO su `remotePath` (solo remoti) per prevenire doppi.
Future<void> ensureUniqueRemotePath() async {
try {
await db.execute(
'CREATE UNIQUE INDEX IF NOT EXISTS uq_entry_remote_remotePath '
'ON entry(remotePath) WHERE origin=1 AND remotePath IS NOT NULL',
);
debugPrint('[RemoteRepository] ensured UNIQUE index on entry(remotePath) for origin=1');
} catch (e, st) {
debugPrint('[RemoteRepository] ensureUniqueRemotePath error: $e\n$st');
}
}
/// Rimuove duplicati remoti, tenendo la riga con id MAX per ciascun `remoteId`.
Future<int> deduplicateRemotes() async {
try {
final deleted = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remoteId IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remoteId IS NOT NULL '
' GROUP BY remoteId'
')',
);
debugPrint('[RemoteRepository] deduplicateRemotes deleted=$deleted');
return deleted;
} catch (e, st) {
debugPrint('[RemoteRepository] deduplicateRemotes error: $e\n$st');
return 0;
}
}
/// Rimuove duplicati per `remotePath` (exact match), tenendo l'ultima riga.
Future<int> deduplicateByRemotePath() async {
try {
final deleted = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
debugPrint('[RemoteRepository] deduplicateByRemotePath deleted=$deleted');
return deleted;
} catch (e, st) {
debugPrint('[RemoteRepository] deduplicateByRemotePath error: $e\n$st');
return 0;
}
}
/// Helper combinato: prima pulisce i doppioni, poi impone lunicità.
Future<void> sanitizeRemotes() async {
await deduplicateRemotes();
await deduplicateByRemotePath();
await ensureUniqueRemoteId();
await ensureUniqueRemotePath();
}
// =========================
// Utils
// =========================
Future<int> countRemote() async {
final rows = await db.rawQuery('SELECT COUNT(1) AS c FROM entry WHERE origin=1');
return (rows.first['c'] as int?) ?? 0;
}
}

View file

@ -1,26 +1,29 @@
// lib/remote/remote_settings.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class RemoteSettings {
static const _storage = FlutterSecureStorage();
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
resetOnError: true, // auto-reset della singola voce cifrata se fallisce la decrittazione
),
);
// Keys
static const _kEnabled = 'remote_enabled';
static const _kBaseUrl = 'remote_base_url';
static const _kIndexPath = 'remote_index_path';
static const _kEmail = 'remote_email';
static const _kPassword = 'remote_password';
// Default values:
// In DEBUG vogliamo valori pre-compilati; in RELEASE lasciamo vuoti/false.
static final bool defaultEnabled = kDebugMode ? true : false;
static final String defaultBaseUrl = kDebugMode ? 'https://prova.patachina.it' : '';
static final String defaultIndexPath = kDebugMode ? 'photos/' : '';
static final String defaultEmail = kDebugMode ? 'fabio@gmail.com' : '';
static final String defaultPassword = kDebugMode ? 'master66' : '';
bool enabled;
bool enabled;
String baseUrl;
String indexPath;
String email;
@ -34,16 +37,38 @@ class RemoteSettings {
required this.password,
});
/// Carica i setting dal secure storage.
/// Se un valore non esiste, usa i default (in debug: quelli precompilati).
// 🔎 helper: leggi una chiave in modo safe e, se fallisce, cancella solo quella
static Future<String?> _readKeySafe(String key) async {
try {
return await _storage.read(key: key);
} on PlatformException {
// solo questa chiave è corrotta la pulisco
await _storage.delete(key: key);
return null;
}
}
// 🧼 helper: rimuove caratteri invisibili/di controllo tipici che sporcano gli URL
static String _sanitizeUrl(String s) {
// rimuove BOM, LRM/RLM e altri format characters comuni negli incolla
const _invisibles = r'[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]';
final cleaned = s.replaceAll(RegExp(_invisibles), '');
return cleaned.trim();
}
static Future<RemoteSettings> load() async {
final enabledStr = await _storage.read(key: _kEnabled);
final baseUrl = await _storage.read(key: _kBaseUrl) ?? defaultBaseUrl;
final indexPath = await _storage.read(key: _kIndexPath) ?? defaultIndexPath;
final email = await _storage.read(key: _kEmail) ?? defaultEmail;
final password = await _storage.read(key: _kPassword) ?? defaultPassword;
// legge *per singola chiave* con fallback ai default
final enabledStr = await _readKeySafe(_kEnabled);
final rawBase = await _readKeySafe(_kBaseUrl);
final indexPath = await _readKeySafe(_kIndexPath) ?? defaultIndexPath;
final email = await _readKeySafe(_kEmail) ?? defaultEmail;
final password = await _readKeySafe(_kPassword) ?? defaultPassword;
final enabled = (enabledStr ?? (defaultEnabled ? 'true' : 'false')) == 'true';
// sanitize della base URL (toglie caratteri non alfabetici invisibili)
final baseUrl = _sanitizeUrl(rawBase ?? defaultBaseUrl);
return RemoteSettings(
enabled: enabled,
baseUrl: baseUrl,
@ -53,23 +78,27 @@ class RemoteSettings {
);
}
/// Scrive i setting nel secure storage.
Future<void> save() async {
// Sanitize prima di salvare, così evitiamo che restino in storage caratteri strani
await _storage.write(key: _kEnabled, value: enabled ? 'true' : 'false');
await _storage.write(key: _kBaseUrl, value: baseUrl);
await _storage.write(key: _kIndexPath, value: indexPath);
await _storage.write(key: _kEmail, value: email);
await _storage.write(key: _kBaseUrl, value: _sanitizeUrl(baseUrl));
await _storage.write(key: _kIndexPath, value: indexPath.trim());
await _storage.write(key: _kEmail, value: email.trim());
await _storage.write(key: _kPassword, value: password);
}
/// In DEBUG: se un valore non è ancora impostato, inizializzalo con i default.
/// NON sovrascrive valori già presenti (quindi puoi sempre entrare in Settings e cambiare).
static Future<void> debugSeedIfEmpty() async {
if (!kDebugMode) return;
Future<void> _seed(String key, String value) async {
final existing = await _storage.read(key: key);
if (existing == null) {
try {
final existing = await _storage.read(key: key);
if (existing == null) {
await _storage.write(key: key, value: value);
}
} on PlatformException {
// chiave sporca reset di quella sola chiave e poi scrittura
await _storage.delete(key: key);
await _storage.write(key: key, value: value);
}
}
@ -80,4 +109,4 @@ class RemoteSettings {
await _seed(_kEmail, defaultEmail);
await _seed(_kPassword, defaultPassword);
}
}
}

View file

@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'remote_settings.dart';
import 'remote_http.dart';
class RemoteSettingsPage extends StatefulWidget {
const RemoteSettingsPage({super.key});
@override
State<RemoteSettingsPage> createState() => _RemoteSettingsPageState();
}
class _RemoteSettingsPageState extends State<RemoteSettingsPage> {
final _form = GlobalKey<FormState>();
bool _loaded = false;
bool _saving = false;
bool _enabled = RemoteSettings.defaultEnabled;
final _baseUrl = TextEditingController(text: RemoteSettings.defaultBaseUrl);
final _indexPath = TextEditingController(text: RemoteSettings.defaultIndexPath);
@ -21,74 +27,185 @@ class _RemoteSettingsPageState extends State<RemoteSettingsPage> {
_load();
}
@override
void dispose() {
_baseUrl.dispose();
_indexPath.dispose();
_email.dispose();
_password.dispose();
super.dispose();
}
Future<void> _load() async {
final s = await RemoteSettings.load();
setState(() {
_enabled = s.enabled;
_baseUrl.text = s.baseUrl;
_indexPath.text = s.indexPath;
_email.text = s.email;
_password.text = s.password;
});
try {
final s = await RemoteSettings.load();
if (!mounted) return;
setState(() {
_enabled = s.enabled;
_baseUrl.text = s.baseUrl;
_indexPath.text = s.indexPath;
_email.text = s.email;
_password.text = s.password;
_loaded = true;
});
} catch (e) {
// Fail-open: apri comunque con default/blank e notifica
_showSnack('Impossibile leggere le impostazioni sicure: $e');
if (!mounted) return;
setState(() {
_enabled = RemoteSettings.defaultEnabled;
_baseUrl.text = RemoteSettings.defaultBaseUrl;
_indexPath.text = RemoteSettings.defaultIndexPath;
_email.text = '';
_password.text = '';
_loaded = true;
});
}
}
String? _validateBaseUrl(String? v) {
final s = (v ?? '').trim();
if (s.isEmpty) return 'Obbligatorio';
final uri = Uri.tryParse(s);
if (uri == null || !(uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https'))) {
return 'URL non valida (deve iniziare con http/https)';
}
// opzionale: blocca spazi/controlli interni
if (RegExp(r'[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]').hasMatch(s)) {
return 'URL contiene caratteri non validi (invisibili)';
}
return null;
}
String? _validateIndex(String? v) {
final s = (v ?? '').trim();
if (s.isEmpty) return 'Obbligatorio';
return null;
}
Future<void> _save() async {
if (!_form.currentState!.validate()) return;
final s = RemoteSettings(
enabled: _enabled,
baseUrl: _baseUrl.text.trim(),
indexPath: _indexPath.text.trim(),
email: _email.text.trim(),
password: _password.text,
if (!(_form.currentState?.validate() ?? false)) return;
setState(() => _saving = true);
try {
final s = RemoteSettings(
enabled: _enabled,
baseUrl: _baseUrl.text.trim(),
indexPath: _indexPath.text.trim(),
email: _email.text.trim(),
password: _password.text,
);
await s.save();
// forza Aves a usare SUBITO base URL & token aggiornati
await RemoteHttp.refreshFromSettings();
await RemoteHttp.warmUp(); // non bloccante: utile per loggare stato token/base
if (!mounted) return;
_showSnack('Impostazioni remote salvate');
Navigator.of(context).maybePop();
} catch (e) {
if (!mounted) return;
_showSnack('Salvataggio fallito: $e');
} finally {
if (mounted) setState(() => _saving = false);
}
}
void _showSnack(String msg) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.fixed, // evita "floating off screen"
content: Text(msg),
duration: const Duration(seconds: 3),
),
);
await s.save();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Impostazioni remote salvate')));
Navigator.of(context).maybePop();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Remote Settings')),
body: Form(
key: _form,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Abilita sync remoto'),
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
body: !_loaded
? const Center(child: CircularProgressIndicator())
: AbsorbPointer(
absorbing: _saving,
child: Form(
key: _form,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
SwitchListTile(
title: const Text('Abilita sync remoto'),
value: _enabled,
onChanged: (v) => setState(() => _enabled = v),
),
const SizedBox(height: 8),
TextFormField(
controller: _baseUrl,
decoration: const InputDecoration(
labelText: 'Base URL (es. https://server.tld)',
hintText: 'https://example.org',
),
keyboardType: TextInputType.url,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: _validateBaseUrl,
),
const SizedBox(height: 12),
TextFormField(
controller: _indexPath,
decoration: const InputDecoration(
labelText: 'Index path (es. photos/)',
hintText: 'photos/',
),
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: _validateIndex,
),
const SizedBox(height: 12),
TextFormField(
controller: _email,
decoration: const InputDecoration(
labelText: 'User/Email',
hintText: 'utente@example.org',
),
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 12),
TextFormField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _saving ? null : _save,
icon: _saving
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.onPrimary,
),
),
)
: const Icon(Icons.save),
label: Text(_saving ? 'Salvataggio in corso...' : 'Salva'),
),
),
],
),
),
),
TextFormField(
controller: _baseUrl,
decoration: const InputDecoration(labelText: 'Base URL (es. https://server.tld)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _indexPath,
decoration: const InputDecoration(labelText: 'Index path (es. photos/)'),
validator: (v) => (v==null || v.isEmpty) ? 'Obbligatorio' : null,
),
TextFormField(
controller: _email,
decoration: const InputDecoration(labelText: 'User/Email'),
),
TextFormField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _save,
icon: const Icon(Icons.save),
label: const Text('Salva'),
),
],
),
),
);
}
}
}

View file

@ -52,18 +52,25 @@ class _RemoteTestPageState extends State<RemoteTestPage> {
}
Future<void> _init() async {
// 1) Base URL: parametro > settings
final s = await RemoteSettings.load();
final candidate = (widget.baseUrl ?? '').trim();
_baseUrl = candidate.isNotEmpty ? candidate : s.baseUrl.trim();
// 1) Base URL: parametro > settings (fail-open)
try {
final s = await RemoteSettings.load();
final candidate = (widget.baseUrl ?? '').trim();
_baseUrl = candidate.isNotEmpty ? candidate : s.baseUrl.trim();
} catch (_) {
_baseUrl = (widget.baseUrl ?? '').trim(); // se vuoto resterà vuoto
}
// 2) Header Authorization (opzionale)
// 2) Header Authorization (opzionale; fail-open)
_authHeaders = null;
try {
if (_baseUrl.isNotEmpty && (s.email.isNotEmpty || s.password.isNotEmpty)) {
final auth = RemoteAuth(baseUrl: _baseUrl, email: s.email, password: s.password);
final token = await auth.login();
_authHeaders = {'Authorization': 'Bearer $token'};
if (_baseUrl.isNotEmpty) {
final s = await RemoteSettings.load();
if (s.email.isNotEmpty || s.password.isNotEmpty) {
final auth = RemoteAuth(baseUrl: _baseUrl, email: s.email, password: s.password);
final token = await auth.login();
_authHeaders = {'Authorization': 'Bearer $token'};
}
}
} catch (_) {
// In debug non bloccare la pagina se il login immagini fallisce
@ -658,4 +665,4 @@ class _RemoteFullPage extends StatelessWidget {
body: Center(child: body),
);
}
}
}

View file

@ -1,647 +0,0 @@
// lib/remote/remote_test_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sqflite/sqflite.dart';
// Integrazione impostazioni & auth remota (Fase 1)
import 'remote_settings.dart';
import 'auth_client.dart';
import 'url_utils.dart';
enum _RemoteFilter { all, visibleOnly, trashedOnly }
class RemoteTestPage extends StatefulWidget {
final Database db;
/// Base URL preferita (es. https://prova.patachina.it).
/// Se non la passi o è vuota, verrà usata quella in RemoteSettings.
final String? baseUrl;
const RemoteTestPage({
super.key,
required this.db,
this.baseUrl,
});
@override
State<RemoteTestPage> createState() => _RemoteTestPageState();
}
class _RemoteTestPageState extends State<RemoteTestPage> {
Future<List<_RemoteRow>>? _future;
String _baseUrl = '';
Map<String, String>? _authHeaders;
bool _navigating = false; // debounce del tap
_RemoteFilter _filter = _RemoteFilter.all;
// contatori diagnostici
int _countAll = 0;
int _countVisible = 0; // trashed=0
int _countTrashed = 0; // trashed=1
@override
void initState() {
super.initState();
_init(); // prepara baseUrl + header auth (se necessari), poi carica i dati
}
Future<void> _init() async {
// 1) Base URL: parametro > settings
final s = await RemoteSettings.load();
final candidate = (widget.baseUrl ?? '').trim();
_baseUrl = candidate.isNotEmpty ? candidate : s.baseUrl.trim();
// 2) Header Authorization (opzionale)
_authHeaders = null;
try {
if (_baseUrl.isNotEmpty && (s.email.isNotEmpty || s.password.isNotEmpty)) {
final auth = RemoteAuth(baseUrl: _baseUrl, email: s.email, password: s.password);
final token = await auth.login();
_authHeaders = {'Authorization': 'Bearer $token'};
}
} catch (_) {
// In debug non bloccare la pagina se il login immagini fallisce
_authHeaders = null;
}
// 3) Carica contatori e lista
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
}
Future<void> _refreshCounters() async {
// Totale remoti (origin=1), visibili e cestinati
final all = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1",
);
final vis = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=0",
);
final tra = await widget.db.rawQuery(
"SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=1",
);
_countAll = (all.first['c'] as int?) ?? 0;
_countVisible = (vis.first['c'] as int?) ?? 0;
_countTrashed = (tra.first['c'] as int?) ?? 0;
}
Future<List<_RemoteRow>> _load() async {
// Filtro WHERE in base al toggle
String extraWhere = '';
switch (_filter) {
case _RemoteFilter.visibleOnly:
extraWhere = ' AND trashed=0';
break;
case _RemoteFilter.trashedOnly:
extraWhere = ' AND trashed=1';
break;
case _RemoteFilter.all:
default:
extraWhere = '';
}
// Prende le prime 300 entry remote (includiamo il mime e il remoteId)
final rows = await widget.db.rawQuery(
'SELECT id, remoteId, title, remotePath, remoteThumb2, sourceMimeType, trashed '
'FROM entry WHERE origin=1$extraWhere '
'ORDER BY id DESC LIMIT 300',
);
return rows.map((r) {
return _RemoteRow(
id: r['id'] as int,
remoteId: (r['remoteId'] as String?) ?? '',
title: (r['title'] as String?) ?? '',
remotePath: r['remotePath'] as String?,
remoteThumb2: r['remoteThumb2'] as String?,
mime: r['sourceMimeType'] as String?,
trashed: (r['trashed'] as int?) ?? 0,
);
}).toList();
}
// Costruzione robusta dellURL assoluto:
// - se già assoluto ritorna comè
// - se relativo risolve contro _baseUrl (accetta con/senza '/')
String _absUrl(String? relativePath) {
if (relativePath == null || relativePath.isEmpty) return '';
final p = relativePath.trim();
// URL già assoluto
if (p.startsWith('http://') || p.startsWith('https://')) return p;
if (_baseUrl.isEmpty) return '';
try {
final base = Uri.parse(_baseUrl.endsWith('/') ? _baseUrl : '$_baseUrl/');
// normalizza: se inizia con '/', togliamo per usare resolve coerente
final rel = p.startsWith('/') ? p.substring(1) : p;
final resolved = base.resolve(rel);
return resolved.toString();
} catch (_) {
return '';
}
}
bool _isVideo(String? mime, String? path) {
final m = (mime ?? '').toLowerCase();
final p = (path ?? '').toLowerCase();
return m.startsWith('video/') ||
p.endsWith('.mp4') ||
p.endsWith('.mov') ||
p.endsWith('.m4v') ||
p.endsWith('.mkv') ||
p.endsWith('.webm');
}
Future<void> _onRefresh() async {
await _refreshCounters();
_future = _load();
if (mounted) setState(() {});
await _future;
}
Future<void> _diagnosticaDb() async {
try {
final dup = await widget.db.rawQuery('''
SELECT remoteId, COUNT(*) AS cnt
FROM entry
WHERE origin=1 AND remoteId IS NOT NULL
GROUP BY remoteId
HAVING cnt > 1
''');
final vis = await widget.db.rawQuery('''
SELECT COUNT(*) AS visible_remotes
FROM entry
WHERE origin=1 AND trashed=0
''');
final idx = await widget.db.rawQuery("PRAGMA index_list('entry')");
if (!mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Diagnostica DB', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('Duplicati per remoteId:\n${dup.isEmpty ? "nessuno" : dup.map((e)=>e.toString()).join('\n')}'),
const SizedBox(height: 12),
Text('Remoti visibili in Aves (trashed=0): ${vis.first.values.first}'),
const SizedBox(height: 12),
Text('Indici su entry:\n${idx.map((e)=>e.toString()).join('\n')}'),
],
),
),
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Diagnostica DB fallita: $e')),
);
}
}
/// 🔧 Pulisce duplicati per `remotePath` (tiene MAX(id)) e righe senza `remoteId`.
Future<void> _pulisciDuplicatiPath() async {
try {
final delNoId = await widget.db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
final delByPath = await widget.db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
await _onRefresh();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia completata: noId=$delNoId, dupPath=$delByPath')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Pulizia fallita: $e')),
);
}
}
Future<void> _nascondiRemotiInCollection() async {
try {
final changed = await widget.db.rawUpdate('''
UPDATE entry SET trashed=1
WHERE origin=1 AND trashed=0
''');
if (!mounted) return;
await _onRefresh();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Remoti nascosti dalla Collection: $changed')),
);
} on DatabaseException catch (e) {
final msg = e.toString();
if (!mounted) return;
// Probabile connessione R/O: istruisci a riaprire il DB in R/W
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 5),
content: Text(
'UPDATE fallito (DB in sola lettura?): $msg\n'
'Apri il DB in R/W in HomePage._openRemoteTestPage (no readOnly).',
),
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Errore UPDATE: $e')),
);
}
}
@override
Widget build(BuildContext context) {
final ready = (_baseUrl.isNotEmpty && _future != null);
return Scaffold(
appBar: AppBar(
title: const Text('[DEBUG] Remote Test'),
actions: [
IconButton(
icon: const Icon(Icons.bug_report_outlined),
tooltip: 'Diagnostica DB',
onPressed: _diagnosticaDb,
),
IconButton(
icon: const Icon(Icons.cleaning_services_outlined),
tooltip: 'Pulisci duplicati (path)',
onPressed: _pulisciDuplicatiPath,
),
IconButton(
icon: const Icon(Icons.visibility_off_outlined),
tooltip: 'Nascondi remoti in Collection',
onPressed: _nascondiRemotiInCollection,
),
],
),
body: !ready
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// Header contatori + filtro
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 8,
runSpacing: -6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(label: Text('Tot: $_countAll')),
Chip(label: Text('Visibili: $_countVisible')),
Chip(label: Text('Cestinati: $_countTrashed')),
],
),
),
const SizedBox(width: 8),
SegmentedButton<_RemoteFilter>(
segments: const [
ButtonSegment(value: _RemoteFilter.all, label: Text('Tutti')),
ButtonSegment(value: _RemoteFilter.visibleOnly, label: Text('Visibili')),
ButtonSegment(value: _RemoteFilter.trashedOnly, label: Text('Cestinati')),
],
selected: {_filter},
onSelectionChanged: (sel) async {
setState(() => _filter = sel.first);
await _onRefresh();
},
),
],
),
),
const Divider(height: 1),
Expanded(
child: RefreshIndicator(
onRefresh: _onRefresh,
child: FutureBuilder<List<_RemoteRow>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: Center(child: Text('Errore: ${snap.error}')),
),
);
}
final items = snap.data ?? const <_RemoteRow>[];
if (items.isEmpty) {
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: SizedBox(
height: MediaQuery.of(context).size.height * .6,
child: const Center(child: Text('Nessuna entry remota (origin=1)')),
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, mainAxisSpacing: 4, crossAxisSpacing: 4,
),
itemCount: items.length,
itemBuilder: (context, i) {
final it = items[i];
final isVideo = _isVideo(it.mime, it.remotePath);
final thumbUrl = _absUrl(it.remoteThumb2);
final fullUrl = _absUrl(it.remotePath);
final hasThumb = thumbUrl.isNotEmpty;
final hasFull = fullUrl.isNotEmpty;
final heroTag = 'remote_${it.id}';
return GestureDetector(
onLongPress: () async {
if (!context.mounted) return;
await showModalBottomSheet<void>(
context: context,
builder: (_) => Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ID: ${it.id} remoteId: ${it.remoteId} trashed: ${it.trashed}'),
const SizedBox(height: 8),
Text('MIME: ${it.mime}'),
const Divider(),
SelectableText('FULL URL:\n$fullUrl'),
const SizedBox(height: 8),
SelectableText('THUMB URL:\n$thumbUrl'),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
ElevatedButton.icon(
onPressed: hasFull
? () async {
await Clipboard.setData(ClipboardData(text: fullUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('FULL URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy),
label: const Text('Copia FULL'),
),
ElevatedButton.icon(
onPressed: hasThumb
? () async {
await Clipboard.setData(ClipboardData(text: thumbUrl));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('THUMB URL copiato')),
);
}
}
: null,
icon: const Icon(Icons.copy_all),
label: const Text('Copia THUMB'),
),
],
),
],
),
),
),
),
);
},
onTap: () async {
if (_navigating) return; // debounce
_navigating = true;
try {
if (isVideo) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video remoto: anteprima full non disponibile (thumb richiesta).'),
duration: Duration(seconds: 2),
),
);
return;
}
if (!hasFull) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('URL non valido')),
);
return;
}
await Navigator.of(context).push(
PageRouteBuilder(
pageBuilder: (_, __, ___) => _RemoteFullPage(
title: it.title,
url: fullUrl,
headers: _authHeaders,
heroTag: heroTag, // pairing Hero
),
transitionDuration: const Duration(milliseconds: 220),
),
);
} finally {
_navigating = false;
}
},
child: Hero(
tag: heroTag, // pairing Hero
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all(color: Colors.black12)),
child: Stack(
fit: StackFit.expand,
children: [
_buildGridTile(isVideo, thumbUrl, fullUrl),
// Informazioni utili per capire cosa stiamo vedendo
Positioned(
left: 2,
bottom: 2,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
color: Colors.black54,
child: Text(
'id:${it.id} rid:${it.remoteId}${it.trashed==1 ? " (T)" : ""}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
Positioned(
right: 2,
top: 2,
child: Wrap(
spacing: 4,
children: [
if (hasFull)
const _MiniBadge(label: 'URL')
else
const _MiniBadge(label: 'NOURL', color: Colors.red),
if (hasThumb)
const _MiniBadge(label: 'THUMB')
else
const _MiniBadge(label: 'NOTH', color: Colors.orange),
],
),
),
],
),
),
),
);
},
);
},
),
),
),
],
),
);
}
Widget _buildGridTile(bool isVideo, String thumbUrl, String fullUrl) {
if (isVideo) {
// Per i video: NON usiamo Image.network(fullUrl).
// Usiamo la thumb se c'è, altrimenti placeholder con icona "play".
final base = thumbUrl.isEmpty
? const ColoredBox(color: Colors.black12)
: Image.network(
thumbUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
return Stack(
fit: StackFit.expand,
children: [
base,
const Align(
alignment: Alignment.center,
child: Icon(Icons.play_circle_fill, color: Colors.white70, size: 48),
),
],
);
}
// Per le immagini: se non c'è thumb, posso usare direttamente l'URL full.
final displayUrl = thumbUrl.isEmpty ? fullUrl : thumbUrl;
if (displayUrl.isEmpty) {
return const ColoredBox(color: Colors.black12);
}
return Image.network(
displayUrl,
fit: BoxFit.cover,
headers: _authHeaders,
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
);
}
}
class _RemoteRow {
final int id;
final String remoteId;
final String title;
final String? remotePath;
final String? remoteThumb2;
final String? mime;
final int trashed;
_RemoteRow({
required this.id,
required this.remoteId,
required this.title,
this.remotePath,
this.remoteThumb2,
this.mime,
required this.trashed,
});
}
class _MiniBadge extends StatelessWidget {
final String label;
final Color color;
const _MiniBadge({super.key, required this.label, this.color = Colors.black54});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(3),
),
child: Text(label, style: const TextStyle(fontSize: 9, color: Colors.white)),
);
}
}
class _RemoteFullPage extends StatelessWidget {
final String title;
final String url;
final Map<String, String>? headers;
final String heroTag; // pairing Hero
const _RemoteFullPage({
super.key,
required this.title,
required this.url,
required this.heroTag,
this.headers,
});
@override
Widget build(BuildContext context) {
final body = url.isEmpty
? const Text('URL non valido')
: Hero(
tag: heroTag, // pairing con la griglia
child: InteractiveViewer(
maxScale: 5,
child: Image.network(
url,
fit: BoxFit.contain,
headers: headers, // Authorization se il server lo richiede
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 64),
),
),
);
return Scaffold(
appBar: AppBar(title: Text(title.isEmpty ? 'Remote' : title)),
body: Center(child: body),
);
}
}

View file

@ -3,27 +3,68 @@ import 'package:flutter/material.dart';
import 'remote_http.dart';
import 'package:aves/model/entry/entry.dart';
/// Un entry è "remoto" se origin==1
bool isRemote(AvesEntry e) => e.origin == 1;
/// Heuristics base per capire se è video (MIME o estensione)
bool isVideo(AvesEntry e) {
final mt = (e.sourceMimeType ?? '').toLowerCase();
final p = (e.remotePath ?? e.path ?? '').toLowerCase();
return mt.startsWith('video/') || p.endsWith('.mp4') || p.endsWith('.mov') ||
p.endsWith('.webm') || p.endsWith('.mkv');
return mt.startsWith('video/') ||
p.endsWith('.mp4') || p.endsWith('.mov') ||
p.endsWith('.webm') || p.endsWith('.mkv') || p.endsWith('.m4v');
}
Future<Widget> remoteImageFull(AvesEntry e) async {
/// Normalizza la rotazione a {0,90,180,270}.
/// Se non presente o non valida 0.
int rotationDegOf(AvesEntry e) {
final raw = e.sourceRotationDegrees ?? 0;
final n = ((raw % 360) + 360) % 360;
if (n % 90 != 0) return 0;
return n;
}
/// Aspect ratio "effettivo" per il layout:
/// se la rotazione è 90/270 scambia width/height.
double effectiveAspectRatio(AvesEntry e) {
final w = (e.width ?? 0).toDouble();
final h = (e.height ?? 0).toDouble();
if (w <= 0 || h <= 0) return 1.0; // fallback sicuro
final rot = rotationDegOf(e);
final swap = rot == 90 || rot == 270;
return swap ? (h / w) : (w / h);
}
/// Versione compatibile (chiama quella con rotazione).
Future<Widget> remoteImageFull(AvesEntry e) => remoteImageFullWithRotate(e);
/// Mostra l'immagine remota rispettando rotazione e aspect ratio.
/// - Usa AspectRatio per non "schiacciare" l'immagine.
/// - Usa RotatedBox (rotazioni a quarti di giro) per non perdere qualità.
/// - Carica via HTTP + Bearer (RemoteHttp).
Future<Widget> remoteImageFullWithRotate(AvesEntry e) async {
final url = RemoteHttp.absUrl(e.remotePath ?? e.path);
final hdr = await RemoteHttp.headers();
if (url.isEmpty) return const Icon(Icons.broken_image, size: 64);
return InteractiveViewer(
maxScale: 5,
child: Image.network(
url,
fit: BoxFit.contain,
headers: hdr,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 64),
final rot = rotationDegOf(e);
final quarterTurns = (rot ~/ 90) % 4; // 0..3
return Center(
child: AspectRatio(
aspectRatio: effectiveAspectRatio(e),
child: InteractiveViewer(
maxScale: 5,
child: RotatedBox(
quarterTurns: quarterTurns,
child: Image.network(
url,
fit: BoxFit.contain,
headers: hdr,
errorBuilder: (_, __, ___) => const Icon(Icons.broken_image, size: 64),
),
),
),
),
);
}
}

View file

@ -1,171 +1,60 @@
// lib/remote/run_remote_sync.dart
//
// Esegue un ciclo di sincronizzazione "pull":
// 1) legge le impostazioni (server, path, user, password) da RemoteSettings
// 2) login Bearer token
// 3) GET dell'indice JSON (array di oggetti foto)
// 4) upsert nel DB 'entry' (e 'address' se presente) tramite RemoteRepository
//
// NOTE:
// - La versione "managed" (runRemoteSyncOnceManaged) apre/chiude il DB ed evita run concorrenti.
// - La versione "plain" (runRemoteSyncOnce) usa un Database già aperto (compatibilità).
// - PRAGMA per concorrenza (WAL, busy_timeout, ...).
// - Non logghiamo contenuti sensibili (password/token/body completi).
import 'package:path/path.dart' as p;
import 'package:aves/services/common/services.dart';
import 'package:aves/model/db/db.dart'; // LocalMediaDb
import 'package:aves/remote/remote_repository.dart';
import 'package:aves/remote/remote_models.dart'; // RemotePhotoItem
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:sqflite/sqflite.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
import 'remote_client.dart';
import 'remote_repository.dart';
// === Guardia anti-concorrenza (single-flight) per la run "managed" ===
bool _remoteSyncRunning = false;
/// Helper: retry esponenziale breve per SQLITE_BUSY.
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
final msg = e.toString();
final isBusy = msg.contains('SQLITE_BUSY') || msg.contains('database is locked');
if (!isBusy || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
/// Versione "managed":
/// - impedisce run concorrenti
/// - apre/chiude da sola la connessione a `metadata.db` (istanza indipendente)
/// - imposta PRAGMA per concorrenza
/// - accetta override opzionali (utile in test)
Future<void> runRemoteSyncOnceManaged({
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
if (_remoteSyncRunning) {
// ignore: avoid_print
print('[remote-sync] already running, skip');
return;
}
_remoteSyncRunning = true;
Database? db;
try {
final dbDir = await getDatabasesPath();
final dbPath = p.join(dbDir, 'metadata.db');
db = await openDatabase(
dbPath,
singleInstance: false, // connessione indipendente (non chiude lhandle di Aves)
onConfigure: (db) async {
try {
// Alcuni PRAGMA ritornano valori usare SEMPRE rawQuery.
await db.rawQuery('PRAGMA journal_mode=WAL');
await db.rawQuery('PRAGMA synchronous=NORMAL');
await db.rawQuery('PRAGMA busy_timeout=3000');
await db.rawQuery('PRAGMA wal_autocheckpoint=1000');
await db.rawQuery('PRAGMA foreign_keys=ON');
// (Opzionale) verifica del mode corrente
final jm = await db.rawQuery('PRAGMA journal_mode');
final mode = jm.isNotEmpty ? jm.first.values.first : null;
// ignore: avoid_print
print('[remote-sync] journal_mode=$mode'); // atteso: wal
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][WARN] PRAGMA setup failed: $e\n$st');
// Non rilanciare: in estremo, continueremo con journaling di default
}
},
);
await runRemoteSyncOnce(
db: db,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
} finally {
try {
await db?.close();
} catch (_) {
// In caso di close doppio/già chiuso, ignoro.
}
_remoteSyncRunning = false;
}
}
/// Versione "plain":
/// Esegue login, scarica /photos e fa upsert nel DB usando una connessione
/// SQLite **già aperta** (non viene chiusa qui).
///
/// Gli optional [baseUrl], [indexPath], [email], [password] permettono override
/// delle impostazioni salvate in `RemoteSettings` (comodo per test / debug).
Future<void> runRemoteSyncOnce({
required Database db,
String? baseUrl,
String? indexPath,
String? email,
String? password,
/// Esegue un giro di sync remoto riusando **la stessa connessione** del loader (4A).
/// Passa una funzione `fetch` che ritorna la lista di RemotePhotoItem da importare.
/// Ritorna il numero di elementi **nuovi/aggiornati** importati.
Future<int> runRemoteSyncOnceManaged({
required Future<List<RemotePhotoItem>> Function() fetch,
}) async {
try {
// 1) Carica impostazioni sicure (secure storage)
final s = await RemoteSettings.load();
final bUrl = (baseUrl ?? s.baseUrl).trim();
final ip = (indexPath ?? s.indexPath).trim();
final em = (email ?? s.email).trim();
final pw = (password ?? s.password);
if (bUrl.isEmpty || ip.isEmpty) {
throw StateError('Impostazioni remote incomplete: baseUrl/indexPath mancanti');
}
// 2) Autenticazione (Bearer)
final auth = RemoteAuth(baseUrl: bUrl, email: em, password: pw);
await auth.login(); // Se necessario, RemoteJsonClient può riloggare su 401
// 3) Client JSON (segue anche redirect 301/302/307/308)
final client = RemoteJsonClient(bUrl, ip, auth: auth);
// 4) Scarica lelenco di elementi remoti (array top-level)
final items = await client.fetchAll();
// 5) Upsert nel DB (con retry se incappiamo in SQLITE_BUSY)
// 1) Usa la **stessa connessione** del loader
final localDb = getIt<LocalMediaDb>();
final db = localDb.rawDb;
final repo = RemoteRepository(db);
await _withRetryBusy(() => repo.upsertAll(items));
// 5.b) Pulizia + indici (copre sia remoteId sia remotePath)
// 2) Conteggio pre (diagnostica)
final pre = (Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=1')
) ?? 0);
debugPrint('[remote-sync][pre] count origin=1 = $pre');
// 3) Fetch dal server
final items = await fetch();
debugPrint('[remote-sync] fetchAll done, items=${items.length}');
if (items.isEmpty) {
debugPrint('[remote-sync] no items to import, exiting');
return 0;
}
// 4) Upsert + sanitize
final sw = Stopwatch()..start();
await repo.upsertAll(items);
await repo.sanitizeRemotes();
sw.stop();
debugPrint('[remote-sync] upsert+sanitize completed in ${sw.elapsedMilliseconds}ms');
// 5.c) **Paracadute visibilità remoti**: deve restare DISABILITATO
// (se lo riattivi, i remoti spariscono dalla galleria)
// await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;');
// 5) Conteggio post e sample (diagnostica)
final post = (Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM entry WHERE origin=1')
) ?? 0);
debugPrint('[remote-sync][post] count origin=1 = $post');
// 5.d) (Opzionale) CLEANUP LEGACY: elimina righe remote senza `remoteId`
// utilissimo se hai record vecchi non deduplicabili
final purgedNoId = await db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
final sample = await db.rawQuery(
'SELECT id, remoteId, provider, uri, trashed, dateModifiedMillis '
'FROM entry WHERE origin=1 ORDER BY id DESC LIMIT 3'
);
debugPrint('[remote-sync][post] sample origin=1 = $sample');
// 6) Log sintetico
final count = await repo.countRemote().catchError((_) => null);
// ignore: avoid_print
print('[remote-sync] import completato: remoti=${count ?? 'n/a'} (base=$bUrl, index=$ip, purged(noId)=$purgedNoId)');
return (post - pre).clamp(0, items.length);
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][ERROR] $e\n$st');
rethrow;
debugPrint('[remote-sync] ERROR: $e\n$st');
return 0;
}
}
}

View file

@ -1,194 +0,0 @@
// lib/remote/run_remote_sync.dart
//
// Esegue un ciclo di sincronizzazione "pull":
// 1) legge le impostazioni (server, path, user, password) da RemoteSettings
// 2) login Bearer token
// 3) GET dell'indice JSON (array di oggetti foto)
// 4) upsert nel DB 'entry' (e 'address' se presente) tramite RemoteRepository
//
// NOTE:
// - La versione "managed" (runRemoteSyncOnceManaged) apre/chiude il DB ed evita run concorrenti.
// - La versione "plain" (runRemoteSyncOnce) usa un Database già aperto (compatibilità).
// - PRAGMA per concorrenza (WAL, busy_timeout, ...).
// - Non logghiamo contenuti sensibili (password/token/body completi).
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'remote_settings.dart';
import 'auth_client.dart';
import 'remote_client.dart';
import 'remote_repository.dart';
// === Guardia anti-concorrenza (single-flight) per la run "managed" ===
bool _remoteSyncRunning = false;
/// Helper: retry esponenziale breve per SQLITE_BUSY.
Future<T> _withRetryBusy<T>(Future<T> Function() fn) async {
const maxAttempts = 3;
var delay = const Duration(milliseconds: 250);
for (var i = 0; i < maxAttempts; i++) {
try {
return await fn();
} catch (e) {
final msg = e.toString();
final isBusy = msg.contains('SQLITE_BUSY') || msg.contains('database is locked');
if (!isBusy || i == maxAttempts - 1) rethrow;
await Future.delayed(delay);
delay *= 2; // 250 500 1000 ms
}
}
// non dovrebbe arrivare qui
return await fn();
}
/// Versione "managed":
/// - impedisce run concorrenti
/// - apre/chiude da sola la connessione a `metadata.db` (istanza indipendente)
/// - imposta PRAGMA per concorrenza
/// - accetta override opzionali (utile in test)
Future<void> runRemoteSyncOnceManaged({
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
if (_remoteSyncRunning) {
// ignore: avoid_print
print('[remote-sync] already running, skip');
return;
}
_remoteSyncRunning = true;
Database? db;
try {
final dbDir = await getDatabasesPath();
final dbPath = p.join(dbDir, 'metadata.db');
db = await openDatabase(
dbPath,
singleInstance: false, // connessione indipendente (non chiude lhandle di Aves)
onConfigure: (db) async {
try {
// Alcuni PRAGMA ritornano valori usare SEMPRE rawQuery.
await db.rawQuery('PRAGMA journal_mode=WAL');
await db.rawQuery('PRAGMA synchronous=NORMAL');
await db.rawQuery('PRAGMA busy_timeout=3000');
await db.rawQuery('PRAGMA wal_autocheckpoint=1000');
await db.rawQuery('PRAGMA foreign_keys=ON');
// (Opzionale) verifica del mode corrente
final jm = await db.rawQuery('PRAGMA journal_mode');
final mode = jm.isNotEmpty ? jm.first.values.first : null;
// ignore: avoid_print
print('[remote-sync] journal_mode=$mode'); // atteso: wal
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][WARN] PRAGMA setup failed: $e\n$st');
// Non rilanciare: in estremo, continueremo con journaling di default
}
},
);
await runRemoteSyncOnce(
db: db,
baseUrl: baseUrl,
indexPath: indexPath,
email: email,
password: password,
);
} finally {
try {
await db?.close();
} catch (_) {
// In caso di close doppio/già chiuso, ignoro.
}
_remoteSyncRunning = false;
}
}
/// Versione "plain":
/// Esegue login, scarica /photos e fa upsert nel DB usando una connessione
/// SQLite **già aperta** (non viene chiusa qui).
///
/// Gli optional [baseUrl], [indexPath], [email], [password] permettono override
/// delle impostazioni salvate in `RemoteSettings` (comodo per test / debug).
Future<void> runRemoteSyncOnce({
required Database db,
String? baseUrl,
String? indexPath,
String? email,
String? password,
}) async {
try {
// 1) Carica impostazioni sicure (secure storage)
final s = await RemoteSettings.load();
final bUrl = (baseUrl ?? s.baseUrl).trim();
final ip = (indexPath ?? s.indexPath).trim();
final em = (email ?? s.email).trim();
final pw = (password ?? s.password);
if (bUrl.isEmpty || ip.isEmpty) {
throw StateError('Impostazioni remote incomplete: baseUrl/indexPath mancanti');
}
// 2) Autenticazione (Bearer)
final auth = RemoteAuth(baseUrl: bUrl, email: em, password: pw);
await auth.login(); // Se necessario, RemoteJsonClient può riloggare su 401
// 3) Client JSON (segue anche redirect 301/302/307/308)
final client = RemoteJsonClient(bUrl, ip, auth: auth);
// 4) Scarica lelenco di elementi remoti (array top-level)
final items = await client.fetchAll();
// 5) Upsert nel DB (con retry se incappiamo in SQLITE_BUSY)
final repo = RemoteRepository(db);
await _withRetryBusy(() => repo.upsertAll(items));
// 5.b) Impedisci futuri duplicati e ripulisci quelli già presenti
await repo.ensureUniqueRemoteId();
final removed = await repo.deduplicateRemotes();
// 5.c) Paracadute: assicura che i remoti NON siano mostrati nella Collection Aves
//await db.rawUpdate('UPDATE entry SET trashed=1 WHERE origin=1 AND trashed=0;');
// 5.d) **CLEANUP LEGACY**: elimina righe remote "orfane" o doppioni su remotePath
// - Righe senza remoteId (NULL o vuoto): non deduplicabili via UNIQUE vanno rimosse
final purgedNoId = await db.rawDelete(
"DELETE FROM entry WHERE origin=1 AND (remoteId IS NULL OR TRIM(remoteId)='')",
);
// - Doppioni per remotePath: tieni solo la riga con id MAX
// (copre i casi in cui in passato siano state create due righe per lo stesso path)
final purgedByPath = await db.rawDelete(
'DELETE FROM entry '
'WHERE origin=1 AND remotePath IS NOT NULL AND id NOT IN ('
' SELECT MAX(id) FROM entry '
' WHERE origin=1 AND remotePath IS NOT NULL '
' GROUP BY remotePath'
')',
);
// ignore: avoid_print
print('[remote-sync] cleanup: removed dup(remoteId)=$removed, purged(noId)=$purgedNoId, purged(byPath)=$purgedByPath');
// 6) Log sintetico
int? c;
try {
c = await repo.countRemote();
} catch (_) {
c = null;
}
// ignore: avoid_print
if (c == null) {
print('[remote-sync] import completato (conteggio non disponibile)');
} else {
print('[remote-sync] importati remoti: $c (base=$bUrl, index=$ip)');
}
} catch (e, st) {
// ignore: avoid_print
print('[remote-sync][ERROR] $e\n$st');
rethrow;
}
}

View file

@ -1,6 +1,7 @@
// lib/widgets/home/home_page.dart
import 'dart:async';
import 'package:aves/remote/collection_source_remote_ext.dart';
import 'package:aves/app_mode.dart';
import 'package:aves/geo/uri.dart';
import 'package:aves/model/app/intent.dart';
@ -46,13 +47,22 @@ import 'package:latlong2/latlong.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
// --- IMPORT aggiunti/aggiornati per integrazione remota (Fase 1) ---
// --- IMPORT aggiunti per integrazione remota / telemetria ---
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:aves/remote/remote_test_page.dart' as rtp;
import 'package:aves/remote/run_remote_sync.dart' as rrs;
import 'package:aves/remote/remote_settings.dart';
import 'package:aves/remote/remote_http.dart'; // PERF/REMOTE: warm-up headers
import 'package:aves/remote/remote_models.dart'; // RemotePhotoItem
// --- IMPORT per client reale ---
import 'package:aves/remote/remote_client.dart';
import 'package:aves/remote/auth_client.dart';
// secure storage import (used only in debug helper)
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class HomePage extends StatefulWidget {
static const routeName = '/';
@ -80,6 +90,8 @@ class _HomePageState extends State<HomePage> {
// guard UI per schedulare UNA sola run del sync da Home
bool _remoteSyncScheduled = false;
// indica se il sync è effettivamente in corso
bool _remoteSyncActive = false;
// guard per evitare doppi push della pagina di test remota
bool _remoteTestOpen = false;
@ -132,6 +144,19 @@ class _HomePageState extends State<HomePage> {
await availability.onNewIntent();
await androidFileUtils.init();
// PERF/REMOTE: warm-up headers (Bearer) in background safe version
unawaited(Future(() async {
try {
final s = await _safeLoadRemoteSettings();
if (s.enabled && s.baseUrl.trim().isNotEmpty) {
await _safeHeaders(); // popola la cache per peekHeaders() in modo sicuro
debugPrint('[startup] remote headers warm-up done (safe)');
}
} catch (e) {
debugPrint('[startup] remote headers warm-up skipped: $e');
}
}));
if (!{
IntentActions.edit,
IntentActions.screenSaver,
@ -257,22 +282,53 @@ class _HomePageState extends State<HomePage> {
);
final loadTopEntriesFirst =
settings.homeNavItem.route == CollectionPage.routeName && settings.homeCustomCollection.isEmpty;
source.canAnalyze = true;
// PERF: UI-first niente analisi prima della prima paint
source.canAnalyze = false;
final swInit = Stopwatch()..start();
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
swInit.stop();
debugPrint('[startup] source.init done in ${swInit.elapsedMilliseconds}ms');
}
// REMOTE: unisci alla sorgente anche gli elementi remoti (origin=1, non cestinati)
await source.appendRemoteEntries();
// REMOTE: aggiungi remoti visibili (origin=1, trashed=0)
final swAppend1 = Stopwatch()..start();
await source.appendRemoteEntriesFromDb();
swAppend1.stop();
debugPrint('[startup] appendRemoteEntries (pre-sync) in ${swAppend1.elapsedMilliseconds}ms');
// === FASE 1: SYNC REMOTO POST-INIT (non blocca la UI) ===
// In DEBUG: fai seed dei settings se sono vuoti, poi lancia il sync SOLO quando
// la sorgente ha finito il loading, con un micro delay di sicurezza.
// === DIAGNOSTICA PRE- SYNC ===
await _printRemoteDiag(source, when: ' PRE');
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// PATCH A: se ci sono remoti in DB, forza la Collection "All items"
try {
final remCount = (await localMediaDb.rawDb
.rawQuery('SELECT COUNT(*) AS c FROM entry WHERE origin=1 AND trashed=0'))
.first['c'] as int? ?? 0;
if (remCount > 0) {
_initialRouteName = CollectionPage.routeName;
_initialFilters = <CollectionFilter>{}; // All items (nessun filtro)
debugPrint('[startup] forcing CollectionPage All-items (remoti=$remCount)');
}
} catch (e) {
debugPrint('[startup] unable to count remotes: $e');
}
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// PERF: riattiva lanalisi in background appena la UI è pronta
unawaited(Future.delayed(const Duration(milliseconds: 300)).then((_) {
source.canAnalyze = true;
debugPrint('[startup] analysis re-enabled in background');
}));
// === SYNC REMOTO post-init (non blocca la UI) ===
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true; // una sola schedulazione per avvio
unawaited(Future(() async {
try {
await RemoteSettings.debugSeedIfEmpty();
final rs = await RemoteSettings.load();
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled) return;
// attesa fine loading
@ -295,15 +351,41 @@ class _HomePageState extends State<HomePage> {
// piccolo margine per step secondari (tag, ecc.)
await Future.delayed(const Duration(milliseconds: 400));
// sync in background (la managed ha già il suo guard interno)
await rrs.runRemoteSyncOnceManaged();
// SYNC su **stessa connessione** + FETCH (obbligatorio)
debugPrint('[remote-sync] START (scheduled=$_remoteSyncScheduled)');
_remoteSyncActive = true;
try {
final swSync = Stopwatch()..start();
final imported = await rrs.runRemoteSyncOnceManaged(
fetch: _fetchAllRemoteItems, // 👈 PASSIAMO IL FETCH
).timeout(const Duration(seconds: 60)); // timeout regolabile
swSync.stop();
debugPrint('[remote-sync] completed in ${swSync.elapsedMilliseconds}ms, imported=$imported');
} on TimeoutException catch (e) {
debugPrint('[remote-sync] TIMEOUT after 60s: $e');
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
} finally {
_remoteSyncActive = false;
debugPrint('[remote-sync] END (active=$_remoteSyncActive)');
}
// REMOTE: dopo il sync, riallinea la sorgente con gli entry remoti
// REMOTE: dopo il sync, append di eventuali nuovi remoti
if (mounted) {
await source.appendRemoteEntries();
final swAppend2 = Stopwatch()..start();
await source.appendRemoteEntriesFromDb();
swAppend2.stop();
debugPrint('[remote-sync] appendRemoteEntries (post-sync) in ${swAppend2.elapsedMilliseconds}ms');
// 🔎 Conteggio di debug usando una CollectionLens temporanea
final c = _countRemotesInSource(source);
debugPrint('[check] remoti in CollectionSource = $c');
// === DIAGNOSTICA POST- SYNC ===
await _printRemoteDiag(source, when: ' POST');
}
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
debugPrint('[remote-sync] outer error: $e\n$st');
}
}));
}
@ -314,8 +396,6 @@ class _HomePageState extends State<HomePage> {
final source2 = context.read<CollectionSource>();
source2.canAnalyze = false;
await source2.init(scope: settings.screenSaverCollectionFilters);
// (Opzionale) mostra anche remoti nello screensaver:
// await source2.appendRemoteEntries(notify: false);
break;
case AppMode.view:
@ -328,9 +408,6 @@ class _HomePageState extends State<HomePage> {
// analysis is necessary to display neighbour items when the initial item is a new one
source.canAnalyze = true;
await source.init(scope: {StoredAlbumFilter(directory, null)});
// (facoltativo) includi remoti anche nel lens di view directory:
// await source.appendRemoteEntries();
}
} else {
await _initViewerEssentials();
@ -362,6 +439,63 @@ class _HomePageState extends State<HomePage> {
}
}
// === FETCH per il sync (implementazione reale usando RemoteJsonClient) ===
Future<List<RemotePhotoItem>> _fetchAllRemoteItems() async {
try {
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled || rs.baseUrl.trim().isEmpty) {
debugPrint('[remote-sync][fetch] disabled or baseUrl empty');
return <RemotePhotoItem>[];
}
// Costruisci l'auth solo se sono presenti credenziali
RemoteAuth? auth;
if (rs.email.isNotEmpty && rs.password.isNotEmpty) {
auth = RemoteAuth(baseUrl: rs.baseUrl, email: rs.email, password: rs.password);
}
final client = RemoteJsonClient(rs.baseUrl, rs.indexPath, auth: auth);
try {
final items = await client.fetchAll();
debugPrint('[remote-sync][fetch] fetched ${items.length} items from ${rs.baseUrl}');
return items;
} catch (e, st) {
debugPrint('[remote-sync][fetch] client.fetchAll ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
} catch (e, st) {
debugPrint('[remote-sync][fetch] ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
}
// --- Helper di debug: crea una lens temporanea, conta i remoti, poi dispose
int _countRemotesInSource(CollectionSource source) {
final lens = CollectionLens(source: source, filters: {});
try {
return lens.sortedEntries.where((e) => e.origin == 1 && e.trashed == 0).length;
} finally {
lens.dispose();
}
}
// === DIAG: stampa conteggi remoti DB/Source/visibleEntries ===
Future<void> _printRemoteDiag(CollectionSource source, {String when = ''}) async {
try {
final dbRem = await localMediaDb.loadEntries(origin: 1);
final dbCount = dbRem.length;
final displayable = dbRem.where((e) => !e.trashed && e.isDisplayable).length;
final inSource = source.allEntries.where((e) => e.origin == 1 && !e.trashed).length;
final inVisible = source.visibleEntries.where((e) => e.origin == 1 && !e.trashed).length;
debugPrint('[diag$when] DB remoti=$dbCount, displayable=$displayable, '
'inSource=$inSource, inVisible=$inVisible');
} catch (e, st) {
debugPrint('[diag$when] ERROR: $e\n$st');
}
}
Future<void> _initViewerEssentials() async {
// for video playback storage
await localMediaDb.init();
@ -389,6 +523,14 @@ class _HomePageState extends State<HomePage> {
// === DEBUG: apre la pagina di test remota con una seconda connessione al DB ===
Future<void> _openRemoteTestPage(BuildContext context) async {
if (_remoteTestOpen) return; // evita doppi push/sovrapposizioni
// blocca solo se il sync è effettivamente in corso
if (_remoteSyncActive) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Attendi fine sync remoto in corso prima di aprire RemoteTest')),
);
return;
}
_remoteTestOpen = true;
Database? debugDb;
@ -407,7 +549,7 @@ class _HomePageState extends State<HomePage> {
);
if (!context.mounted) return;
final rs = await RemoteSettings.load();
final rs = await _safeLoadRemoteSettings();
final baseUrl = rs.baseUrl.isNotEmpty ? rs.baseUrl : RemoteSettings.defaultBaseUrl;
await Navigator.of(context).push(MaterialPageRoute(
@ -433,7 +575,7 @@ class _HomePageState extends State<HomePage> {
// === DEBUG: dialog impostazioni remote (semplice) ===
Future<void> _openRemoteSettingsDialog(BuildContext context) async {
final s = await RemoteSettings.load();
final s = await _safeLoadRemoteSettings();
final formKey = GlobalKey<FormState>();
bool enabled = s.enabled;
final baseUrlC = TextEditingController(text: s.baseUrl);
@ -508,6 +650,11 @@ class _HomePageState extends State<HomePage> {
password: pwC.text,
);
await upd.save();
// forza refresh immediato delle impostazioni e headers
await RemoteHttp.refreshFromSettings();
unawaited(RemoteHttp.warmUp());
if (context.mounted) Navigator.of(context).pop();
if (context.mounted) {
ScaffoldMessenger.of(context)
@ -699,7 +846,7 @@ class _HomePageState extends State<HomePage> {
);
case CollectionPage.routeName:
default:
// <<--- Wrapper di debug che aggiunge i due FAB (solo in debug)
// Wrapper di debug che aggiunge i due FAB (solo in debug)
return buildRoute(
(context) => _wrapWithRemoteDebug(
context,
@ -708,4 +855,51 @@ class _HomePageState extends State<HomePage> {
);
}
}
// -------------------------
// Utility sicure per remote
// -------------------------
// safe load of RemoteSettings with timeout and fallback
Future<RemoteSettings> _safeLoadRemoteSettings({Duration timeout = const Duration(seconds: 5)}) async {
try {
return await RemoteSettings.load().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteSettings.load failed: $e — using defaults');
return RemoteSettings(
enabled: RemoteSettings.defaultEnabled,
baseUrl: RemoteSettings.defaultBaseUrl,
indexPath: RemoteSettings.defaultIndexPath,
email: RemoteSettings.defaultEmail,
password: RemoteSettings.defaultPassword,
);
}
}
// safe headers retrieval with timeout and empty fallback
Future<Map<String, String>> _safeHeaders({Duration timeout = const Duration(seconds: 6)}) async {
try {
return await RemoteHttp.headers().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteHttp.headers failed: $e — returning empty headers');
return const {};
}
}
// debug helper: clear remote keys from secure storage (debug only)
Future<void> _debugClearRemoteKeys() async {
if (!kDebugMode) return;
try {
// FlutterSecureStorage non è const
final storage = FlutterSecureStorage();
await storage.delete(key: 'remote_base_url');
await storage.delete(key: 'remote_index_path');
await storage.delete(key: 'remote_email');
await storage.delete(key: 'remote_password');
await storage.delete(key: 'remote_enabled');
debugPrint('[remote] debugClearRemoteKeys executed');
} catch (e) {
debugPrint('[remote] debugClearRemoteKeys failed: $e');
}
}
}

View file

@ -1,6 +1,7 @@
// lib/widgets/home/home_page.dart
import 'dart:async';
import 'package:aves/remote/collection_source_remote_ext.dart';
import 'package:aves/app_mode.dart';
import 'package:aves/geo/uri.dart';
import 'package:aves/model/app/intent.dart';
@ -46,13 +47,22 @@ import 'package:latlong2/latlong.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
// --- IMPORT aggiunti/aggiornati per integrazione remota (Fase 1) ---
// --- IMPORT aggiunti per integrazione remota / telemetria ---
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:aves/remote/remote_test_page.dart' as rtp;
import 'package:aves/remote/run_remote_sync.dart' as rrs;
import 'package:aves/remote/remote_settings.dart';
import 'package:aves/remote/remote_http.dart'; // PERF/REMOTE: warm-up headers
import 'package:aves/remote/remote_models.dart'; // RemotePhotoItem
// --- IMPORT per client reale ---
import 'package:aves/remote/remote_client.dart';
import 'package:aves/remote/auth_client.dart';
// secure storage import (used only in debug helper)
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class HomePage extends StatefulWidget {
static const routeName = '/';
@ -80,6 +90,8 @@ class _HomePageState extends State<HomePage> {
// guard UI per schedulare UNA sola run del sync da Home
bool _remoteSyncScheduled = false;
// indica se il sync è effettivamente in corso
bool _remoteSyncActive = false;
// guard per evitare doppi push della pagina di test remota
bool _remoteTestOpen = false;
@ -132,6 +144,19 @@ class _HomePageState extends State<HomePage> {
await availability.onNewIntent();
await androidFileUtils.init();
// PERF/REMOTE: warm-up headers (Bearer) in background — safe version
unawaited(Future(() async {
try {
final s = await _safeLoadRemoteSettings();
if (s.enabled && s.baseUrl.trim().isNotEmpty) {
await _safeHeaders(); // popola la cache per peekHeaders() in modo sicuro
debugPrint('[startup] remote headers warm-up done (safe)');
}
} catch (e) {
debugPrint('[startup] remote headers warm-up skipped: $e');
}
}));
if (!{
IntentActions.edit,
IntentActions.screenSaver,
@ -257,19 +282,34 @@ class _HomePageState extends State<HomePage> {
);
final loadTopEntriesFirst =
settings.homeNavItem.route == CollectionPage.routeName && settings.homeCustomCollection.isEmpty;
source.canAnalyze = true;
// PERF: UI-first → niente analisi prima della prima paint
source.canAnalyze = false;
final swInit = Stopwatch()..start();
await source.init(scope: CollectionSource.fullScope, loadTopEntriesFirst: loadTopEntriesFirst);
swInit.stop();
debugPrint('[startup] source.init done in ${swInit.elapsedMilliseconds}ms');
}
// === FASE 1: SYNC REMOTO POST-INIT (non blocca la UI) ===
// In DEBUG: fai seed dei settings se sono vuoti, poi lancia il sync SOLO quando
// la sorgente ha finito il loading, con un micro delay di sicurezza.
// REMOTE: aggiungi remoti visibili (origin=1, trashed=0)
final swAppend1 = Stopwatch()..start();
await source.appendRemoteEntriesFromDb();
swAppend1.stop();
debugPrint('[startup] appendRemoteEntries (pre-sync) in ${swAppend1.elapsedMilliseconds}ms');
// PERF: riattiva lanalisi in background appena la UI è pronta
unawaited(Future.delayed(const Duration(milliseconds: 300)).then((_) {
source.canAnalyze = true;
debugPrint('[startup] analysis re-enabled in background');
}));
// === SYNC REMOTO post-init (non blocca la UI) ===
if (!_remoteSyncScheduled) {
_remoteSyncScheduled = true; // una sola schedulazione per avvio
unawaited(Future(() async {
try {
await RemoteSettings.debugSeedIfEmpty();
final rs = await RemoteSettings.load();
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled) return;
// attesa fine loading
@ -292,10 +332,38 @@ class _HomePageState extends State<HomePage> {
// piccolo margine per step secondari (tag, ecc.)
await Future.delayed(const Duration(milliseconds: 400));
// sync in background (la managed ha già il suo guard interno)
await rrs.runRemoteSyncOnceManaged();
// ⬇️ SYNC su **stessa connessione** + FETCH (obbligatorio)
debugPrint('[remote-sync] START (scheduled=$_remoteSyncScheduled)');
_remoteSyncActive = true;
try {
final swSync = Stopwatch()..start();
final imported = await rrs.runRemoteSyncOnceManaged(
fetch: _fetchAllRemoteItems, // 👈 PASSIAMO IL FETCH
).timeout(const Duration(seconds: 60)); // timeout regolabile
swSync.stop();
debugPrint('[remote-sync] completed in ${swSync.elapsedMilliseconds}ms, imported=$imported');
} on TimeoutException catch (e) {
debugPrint('[remote-sync] TIMEOUT after 60s: $e');
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
} finally {
_remoteSyncActive = false;
debugPrint('[remote-sync] END (active=$_remoteSyncActive)');
}
// REMOTE: dopo il sync, append di eventuali nuovi remoti
if (mounted) {
final swAppend2 = Stopwatch()..start();
await source.appendRemoteEntriesFromDb();
swAppend2.stop();
debugPrint('[remote-sync] appendRemoteEntries (post-sync) in ${swAppend2.elapsedMilliseconds}ms');
// 🔎 Conteggio di debug usando una CollectionLens temporanea
final c = _countRemotesInSource(source);
debugPrint('[check] remoti in CollectionSource = $c');
}
} catch (e, st) {
debugPrint('[remote-sync] error: $e\n$st');
debugPrint('[remote-sync] outer error: $e\n$st');
}
}));
}
@ -349,6 +417,47 @@ class _HomePageState extends State<HomePage> {
}
}
// === FETCH per il sync (implementazione reale usando RemoteJsonClient) ===
Future<List<RemotePhotoItem>> _fetchAllRemoteItems() async {
try {
final rs = await _safeLoadRemoteSettings();
if (!rs.enabled || rs.baseUrl.trim().isEmpty) {
debugPrint('[remote-sync][fetch] disabled or baseUrl empty');
return <RemotePhotoItem>[];
}
// Costruisci l'auth solo se sono presenti credenziali
RemoteAuth? auth;
if (rs.email.isNotEmpty && rs.password.isNotEmpty) {
auth = RemoteAuth(baseUrl: rs.baseUrl, email: rs.email, password: rs.password);
}
final client = RemoteJsonClient(rs.baseUrl, rs.indexPath, auth: auth);
try {
final items = await client.fetchAll();
debugPrint('[remote-sync][fetch] fetched ${items.length} items from ${rs.baseUrl}');
return items;
} catch (e, st) {
debugPrint('[remote-sync][fetch] client.fetchAll ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
} catch (e, st) {
debugPrint('[remote-sync][fetch] ERROR: $e\n$st');
return <RemotePhotoItem>[];
}
}
// --- Helper di debug: crea una lens temporanea, conta i remoti, poi dispose
int _countRemotesInSource(CollectionSource source) {
final lens = CollectionLens(source: source, filters: {});
try {
return lens.sortedEntries.where((e) => e.origin == 1 && e.trashed == 0).length;
} finally {
lens.dispose();
}
}
Future<void> _initViewerEssentials() async {
// for video playback storage
await localMediaDb.init();
@ -376,6 +485,14 @@ class _HomePageState extends State<HomePage> {
// === DEBUG: apre la pagina di test remota con una seconda connessione al DB ===
Future<void> _openRemoteTestPage(BuildContext context) async {
if (_remoteTestOpen) return; // evita doppi push/sovrapposizioni
// blocca solo se il sync è effettivamente in corso
if (_remoteSyncActive) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Attendi fine sync remoto in corso prima di aprire RemoteTest')),
);
return;
}
_remoteTestOpen = true;
Database? debugDb;
@ -394,7 +511,7 @@ class _HomePageState extends State<HomePage> {
);
if (!context.mounted) return;
final rs = await RemoteSettings.load();
final rs = await _safeLoadRemoteSettings();
final baseUrl = rs.baseUrl.isNotEmpty ? rs.baseUrl : RemoteSettings.defaultBaseUrl;
await Navigator.of(context).push(MaterialPageRoute(
@ -420,7 +537,7 @@ class _HomePageState extends State<HomePage> {
// === DEBUG: dialog impostazioni remote (semplice) ===
Future<void> _openRemoteSettingsDialog(BuildContext context) async {
final s = await RemoteSettings.load();
final s = await _safeLoadRemoteSettings();
final formKey = GlobalKey<FormState>();
bool enabled = s.enabled;
final baseUrlC = TextEditingController(text: s.baseUrl);
@ -495,6 +612,11 @@ class _HomePageState extends State<HomePage> {
password: pwC.text,
);
await upd.save();
// forza refresh immediato delle impostazioni e headers
await RemoteHttp.refreshFromSettings();
unawaited(RemoteHttp.warmUp());
if (context.mounted) Navigator.of(context).pop();
if (context.mounted) {
ScaffoldMessenger.of(context)
@ -686,7 +808,7 @@ class _HomePageState extends State<HomePage> {
);
case CollectionPage.routeName:
default:
// <<--- Wrapper di debug che aggiunge i due FAB (solo in debug)
// Wrapper di debug che aggiunge i due FAB (solo in debug)
return buildRoute(
(context) => _wrapWithRemoteDebug(
context,
@ -695,4 +817,51 @@ class _HomePageState extends State<HomePage> {
);
}
}
// -------------------------
// Utility sicure per remote
// -------------------------
// safe load of RemoteSettings with timeout and fallback
Future<RemoteSettings> _safeLoadRemoteSettings({Duration timeout = const Duration(seconds: 5)}) async {
try {
return await RemoteSettings.load().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteSettings.load failed: $e — using defaults');
return RemoteSettings(
enabled: RemoteSettings.defaultEnabled,
baseUrl: RemoteSettings.defaultBaseUrl,
indexPath: RemoteSettings.defaultIndexPath,
email: RemoteSettings.defaultEmail,
password: RemoteSettings.defaultPassword,
);
}
}
// safe headers retrieval with timeout and empty fallback
Future<Map<String, String>> _safeHeaders({Duration timeout = const Duration(seconds: 6)}) async {
try {
return await RemoteHttp.headers().timeout(timeout);
} catch (e) {
debugPrint('[remote] RemoteHttp.headers failed: $e — returning empty headers');
return const {};
}
}
// debug helper: clear remote keys from secure storage (debug only)
Future<void> _debugClearRemoteKeys() async {
if (!kDebugMode) return;
try {
// FlutterSecureStorage non è const
final storage = FlutterSecureStorage();
await storage.delete(key: 'remote_base_url');
await storage.delete(key: 'remote_index_path');
await storage.delete(key: 'remote_email');
await storage.delete(key: 'remote_password');
await storage.delete(key: 'remote_enabled');
debugPrint('[remote] debugClearRemoteKeys executed');
} catch (e) {
debugPrint('[remote] debugClearRemoteKeys failed: $e');
}
}
}

View file

@ -13,7 +13,7 @@ import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:panorama/panorama.dart';
import 'package:panorama_viewer/panorama_viewer.dart';
import 'package:provider/provider.dart';
class PanoramaPage extends StatefulWidget {
@ -34,7 +34,7 @@ class PanoramaPage extends StatefulWidget {
class _PanoramaPageState extends State<PanoramaPage> {
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
final ValueNotifier<SensorControl> _sensorControl = ValueNotifier(SensorControl.None);
final ValueNotifier<SensorControl> _sensorControl = ValueNotifier(SensorControl.none);
AvesEntry get entry => widget.entry;
@ -73,7 +73,7 @@ class _PanoramaPageState extends State<PanoramaPage> {
final croppedArea = info.croppedAreaRect!;
final fullSize = info.fullPanoSize!;
final longitude = ((croppedArea.left + croppedArea.width / 2) / fullSize.width - 1 / 2) * 360;
return Panorama(
return PanoramaViewer(
longitude: longitude,
minZoom: _minZoom,
sensorControl: sensorControl,
@ -84,7 +84,7 @@ class _PanoramaPageState extends State<PanoramaPage> {
child: imageChild,
);
} else {
return Panorama(
return PanoramaViewer(
minZoom: _minZoom,
sensorControl: sensorControl,
onTap: onTap,
@ -139,9 +139,11 @@ class _PanoramaPageState extends State<PanoramaPage> {
valueListenable: _sensorControl,
builder: (context, sensorControl, child) {
return IconButton(
icon: Icon(sensorControl == SensorControl.None ? AIcons.sensorControlEnabled : AIcons.sensorControlDisabled),
icon: Icon(sensorControl == SensorControl.none ? AIcons.sensorControlEnabled : AIcons.sensorControlDisabled),
onPressed: _toggleSensor,
tooltip: sensorControl == SensorControl.None ? context.l10n.panoramaEnableSensorControl : context.l10n.panoramaDisableSensorControl,
tooltip: sensorControl == SensorControl.none
? context.l10n.panoramaEnableSensorControl
: context.l10n.panoramaDisableSensorControl,
);
},
),
@ -155,11 +157,11 @@ class _PanoramaPageState extends State<PanoramaPage> {
void _toggleSensor() {
switch (_sensorControl.value) {
case SensorControl.None:
_sensorControl.value = SensorControl.AbsoluteOrientation;
case SensorControl.AbsoluteOrientation:
case SensorControl.Orientation:
_sensorControl.value = SensorControl.None;
case SensorControl.none:
_sensorControl.value = SensorControl.absoluteOrientation;
case SensorControl.absoluteOrientation:
case SensorControl.orientation:
_sensorControl.value = SensorControl.none;
}
}

View file

@ -23,6 +23,9 @@ class VideoConductor {
static const _defaultMaxControllerCount = 3;
// NUOVO: factory remota (per origin==1)
AvesVideoControllerFactory? _remoteFactory;
VideoConductor({CollectionLens? collection}) : _collection = collection {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectCreated(
@ -33,6 +36,11 @@ class VideoConductor {
}
}
// NUOVO: registrazione factory remota
void registerRemoteFactory(AvesVideoControllerFactory factory) {
_remoteFactory = factory..init();
}
Future<void> dispose() async {
if (kFlutterMemoryAllocationsEnabled) {
LeakTracking.dispatchObjectDisposed(object: this);
@ -55,12 +63,21 @@ class VideoConductor {
await _disposeController(_controllers.removeLast());
}
await deviceService.requestGarbageCollection();
controller = videoControllerFactory.buildController(
// NUOVO: se REMOTO (origin==1) e ho la factory registrata, uso quella;
// altrimenti uso la factory "stock" globale (videoControllerFactory).
final builder = (entry.origin == 1 && _remoteFactory != null)
? _remoteFactory!
: videoControllerFactory;
controller = builder.buildController(
entry,
playbackStateHandler: _playbackStateHandler,
settings: settings,
);
_subscriptions[controller] = controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event));
_subscriptions[controller] =
controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event));
}
_controllers.insert(0, controller);
return controller;
@ -69,7 +86,10 @@ class VideoConductor {
AvesVideoController? getPlayingController() => _controllers.firstWhereOrNull((c) => c.isPlaying);
AvesVideoController? getController(AvesEntry entry) {
return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId);
// 🔒 Robusto anche per remoti: confronta (id, pageId)
return _controllers.firstWhereOrNull(
(c) => c.entry.id == entry.id && c.entry.pageId == entry.pageId,
);
}
Future<void> _onControllerStatusChanged(AvesEntry entry, AvesVideoController controller, VideoStatus status) async {
@ -97,7 +117,8 @@ class VideoConductor {
playingVideoControllerNotifier.value = getPlayingController();
}
Future<void> _applyToAll(Future Function(AvesVideoController controller) action) => Future.forEach<AvesVideoController>(_controllers, action);
Future<void> _applyToAll(Future Function(AvesVideoController controller) action) =>
Future.forEach<AvesVideoController>(_controllers, action);
Future<void> _disposeAll() => _applyToAll(_disposeController);

View file

@ -0,0 +1,31 @@
import 'package:aves/remote/remote_http.dart';
import 'package:aves/model/entry/entry.dart'; // AvesEntry concreto (path/remotePath)
import 'package:aves_model/aves_model.dart'; // espone AvesEntryBase
import 'package:aves_video/aves_video.dart';
class RemoteVideoControllerFactory implements AvesVideoControllerFactory {
@override
void init() {
// opzionale (warm-up o logging)
}
@override
AvesVideoController buildController(
AvesEntryBase entry, {
required PlaybackStateHandler playbackStateHandler,
required VideoSettings settings,
}) {
// Nel tuo app layer lentry è AvesEntry concreto (per path/remotePath)
final e = entry as AvesEntry;
final url = RemoteHttp.absUrl(e.remotePath ?? e.path);
return RemoteNetworkVideoController(
entry,
url: url,
// se la cache non è popolata, il controller farà RemoteHttp.headers()
httpHeaders: RemoteHttp.peekHeaders(),
playbackStateHandler: playbackStateHandler,
settings: settings,
);
}
}

View file

@ -34,11 +34,13 @@ class ViewStateConductor {
set viewportSize(Size size) => _viewportSize = size;
ViewStateController getOrCreateController(AvesEntry entry) {
// Identificazione robusta anche per remoti (uri può essere null):
// usa SEMPRE (id, pageId) come chiave logica del controller.
var controller = getController(entry);
if (controller != null) {
_controllers.remove(controller);
} else {
// try to initialize the view state to match magnifier initial state
// inizializza lo stato per combaciare lo stato iniziale del magnifier
const initialScale = ScaleLevel(ref: ScaleReference.contained);
final initialValue = ViewState(
position: Offset.zero,
@ -66,15 +68,19 @@ class ViewStateConductor {
}
ViewStateController? getController(AvesEntry entry) {
return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId);
// Confronto per (id, pageId) invece di (uri, pageId)
return _controllers.firstWhereOrNull(
(c) => c.entry.id == entry.id && c.entry.pageId == entry.pageId,
);
}
void reset(AvesEntry entry) {
final uris = <AvesEntry>{
entry,
...?entry.stackedEntries,
}.map((v) => v.uri).toSet();
final entryControllers = _controllers.where((v) => uris.contains(v.entry.uri)).toSet();
// Reset per id (gli uri remoti possono essere null, quindi non affidabili)
final ids = <int>{
entry.id,
...?entry.stackedEntries?.map((e) => e.id),
};
final entryControllers = _controllers.where((v) => ids.contains(v.entry.id)).toSet();
entryControllers.forEach((controller) {
_controllers.remove(controller);
controller.dispose();

View file

@ -1,3 +1,4 @@
// lib/widgets/viewer/visual/entry_page_view.dart
import 'dart:async';
import 'package:aves/app_mode.dart';
@ -33,6 +34,9 @@ import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Factory remota per controller video unificato (remoti + locali tramite Conductor)
import 'package:aves/widgets/viewer/video/remote_video_factory.dart';
class EntryPageView extends StatefulWidget {
final AvesEntry mainEntry, pageEntry;
final ViewerController viewerController;
@ -62,11 +66,11 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
OverlayEntry? _actionFeedbackOverlayEntry;
AvesEntry get mainEntry => widget.mainEntry;
AvesEntry get entry => widget.pageEntry;
ViewerController get viewerController => widget.viewerController;
bool get _isRemote => entry.origin == 1;
@override
void initState() {
super.initState();
@ -76,7 +80,6 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
@override
void didUpdateWidget(covariant EntryPageView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.pageEntry != widget.pageEntry) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
@ -100,6 +103,10 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
if (entry.isVideo) {
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
}
// idempotente
context.read<VideoConductor>().registerRemoteFactory(RemoteVideoControllerFactory());
viewerController.startAutopilotAnimation(
vsync: this,
onUpdate: ({required scaleLevel}) {
@ -126,12 +133,17 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
animation: entry.visualChangeNotifier,
builder: (context, child) {
Widget? child;
// Pipeline unificata:
// - SVG _buildSvgView()
// - Video _buildVideoView()
// - Raster (locali + remoti) _buildRasterView()
if (entry.isSvg) {
child = _buildSvgView();
} else if (!entry.displaySize.isEmpty) {
if (entry.isVideo) {
child = _buildVideoView();
} else if (entry.isDecodingSupported) {
} else if (entry.isDecodingSupported || _isRemote) {
child = _buildRasterView();
}
}
@ -146,9 +158,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
if (!settings.viewerUseCutout) {
child = SafeCutoutArea(
child: ClipRect(
child: child,
),
child: ClipRect(child: child),
);
}
@ -167,6 +177,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
return child;
}
// RASTER (locali + remoti)
Widget _buildRasterView() {
return _buildMagnifier(
applyScale: false,
@ -197,6 +208,30 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
);
}
// === Helper: frame Magnifier per i VIDEO senza doppio SAR ===
// - Remoto: usa remoteWidth/remoteHeight, swap se remoteRotation=90/270, NO SAR.
// - Locale: usa il calcolo stock (applica SAR tramite entry.videoDisplaySize).
Size _videoFrameSize(double? sar) {
if (_isRemote) {
double w = (entry.remoteWidth ?? entry.width).toDouble();
double h = (entry.remoteHeight ?? entry.height).toDouble();
final rot90 = ((entry.remoteRotation ?? 0) % 180) == 90;
if (rot90) {
final t = w;
w = h;
h = t;
}
if (w <= 0 || h <= 0) {
final ds = entry.displaySize;
if (!ds.isEmpty) return ds;
return const Size(1920, 1080); // fallback prudente
}
return Size(w, h);
}
// Locali: logica stock Aves (SAR incluso)
return entry.videoDisplaySize(sar);
}
Widget _buildVideoView() {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return const SizedBox();
@ -204,7 +239,8 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
return ValueListenableBuilder<double?>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
// REMOTE: evita doppio SAR nel frame; VideoView applicherà il SAR internamente
final videoDisplaySize = _videoFrameSize(sar);
final isPureVideo = entry.isPureVideo;
return Selector<Settings, (bool, bool, bool)>(
@ -228,12 +264,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
icon?.call() ?? action.getIconData(),
size: 48,
color: Colors.white,
shadows: const [
Shadow(
color: Colors.black,
blurRadius: 4,
),
],
shadows: const [Shadow(color: Colors.black, blurRadius: 4)],
);
VideoActionNotification(
controller: videoController,
@ -372,9 +403,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
controller: coverController,
displaySize: coverSize,
onDoubleTap: onDoubleTap,
child: Image(
image: videoCoverUriImage,
),
child: Image(image: videoCoverUriImage),
),
),
],
@ -385,6 +414,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
);
}
/// Wrapper del Magnifier con bound **remotefriendly**
Widget _buildMagnifier({
AvesMagnifierController? controller,
Size? displaySize,
@ -398,13 +428,22 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
required Widget child,
}) {
final isWallpaperMode = context.read<ValueNotifier<AppMode>>().value == AppMode.setWallpaper;
final minScale = isWallpaperMode ? const ScaleLevel(ref: ScaleReference.covered) : const ScaleLevel(ref: ScaleReference.contained);
const contained = ScaleLevel(ref: ScaleReference.contained);
const covered = ScaleLevel(ref: ScaleReference.covered);
// REMOTO:
// - minScale sotto al fit per abilitare pinchout
// - initialScale al fit (poi RasterImageView lo cappa a <= 1.0)
final minScale = isWallpaperMode
? covered
: (_isRemote ? const ScaleLevel(ref: ScaleReference.contained, factor: .5) : contained);
final initialScale = _isRemote ? contained : viewerController.initialScale;
return ValueListenableBuilder<bool>(
valueListenable: AvesApp.canGestureToOtherApps,
builder: (context, canGestureToOtherApps, child) {
builder: (context, canGestureToOtherApps, childWidget) {
return AvesMagnifier(
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: controller ?? _magnifierController,
contentSize: displaySize ?? entry.displaySize,
@ -412,7 +451,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
allowDoubleTap: _allowDoubleTap,
minScale: minScale,
maxScale: maxScale,
initialScale: viewerController.initialScale,
initialScale: initialScale,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
onScaleStart: onScaleStart,
@ -426,7 +465,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
},
onLongPress: canGestureToOtherApps ? _startGlobalDrag : null,
onDoubleTap: onDoubleTap,
child: child!,
child: childWidget!,
);
},
child: child,
@ -441,16 +480,15 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
final brightness = Theme.of(context).brightness;
final outline = await WidgetOutline.systemBlackAndWhite.color(brightness);
final dragShadowBytes =
await HomeWidgetPainter(
entry: entry,
devicePixelRatio: devicePixelRatio,
).drawWidget(
sizeDip: dragShadowSize,
cornerRadiusPx: cornerRadiusPx,
outline: outline,
shape: WidgetShape.rrect,
);
final dragShadowBytes = await HomeWidgetPainter(
entry: entry,
devicePixelRatio: devicePixelRatio,
).drawWidget(
sizeDip: dragShadowSize,
cornerRadiusPx: cornerRadiusPx,
outline: outline,
shape: WidgetShape.rrect,
);
await windowService.startGlobalDrag(entry.uri, entry.bestTitle, dragShadowSize, dragShadowBytes);
}
@ -487,10 +525,6 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
void _onTap({Alignment? alignment}) => (_handleSideSingleTap(alignment) ?? const ToggleOverlayNotification()).dispatch(context);
// side gesture handling by precedence:
// - seek in video by side double tap (if enabled)
// - go to previous/next entry by side single tap (if enabled)
// - zoom in/out by double tap
bool _allowDoubleTap(Alignment alignment) {
if (entry.isVideo && settings.videoGestureSideDoubleTapSeek) {
return true;

View file

@ -0,0 +1,562 @@
// lib/widgets/viewer/visual/entry_page_view.dart
import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/widget_outline.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/viewer/view_state.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_session_service.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/home_widget.dart';
import 'package:aves/widgets/viewer/controls/controller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/view/conductor.dart';
import 'package:aves/widgets/viewer/visual/error.dart';
import 'package:aves/widgets/viewer/visual/raster.dart';
import 'package:aves/widgets/viewer/visual/vector.dart';
import 'package:aves/widgets/viewer/visual/video/cover.dart';
import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart';
import 'package:aves/widgets/viewer/visual/video/swipe_action.dart';
import 'package:aves/widgets/viewer/visual/video/video_view.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_model/aves_model.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Factory remota per controller video unificato (remoti + locali tramite Conductor)
import 'package:aves/widgets/viewer/video/remote_video_factory.dart';
class EntryPageView extends StatefulWidget {
final AvesEntry mainEntry, pageEntry;
final ViewerController viewerController;
final VoidCallback? onDisposed;
static const decorationCheckSize = 20.0;
static const rasterMaxScale = ScaleLevel(factor: 5);
static const vectorMaxScale = ScaleLevel(factor: 25);
const EntryPageView({
super.key,
required this.mainEntry,
required this.pageEntry,
required this.viewerController,
this.onDisposed,
});
@override
State<EntryPageView> createState() => _EntryPageViewState();
}
class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateMixin {
late ValueNotifier<ViewState> _viewStateNotifier;
late AvesMagnifierController _magnifierController;
final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
OverlayEntry? _actionFeedbackOverlayEntry;
AvesEntry get mainEntry => widget.mainEntry;
AvesEntry get entry => widget.pageEntry;
ViewerController get viewerController => widget.viewerController;
bool get _isRemote => entry.origin == 1;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant EntryPageView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.pageEntry != widget.pageEntry) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget(widget);
widget.onDisposed?.call();
_actionFeedbackChildNotifier.dispose();
super.dispose();
}
void _registerWidget(EntryPageView widget) {
final entry = widget.pageEntry;
_viewStateNotifier = context.read<ViewStateConductor>().getOrCreateController(entry).viewStateNotifier;
_magnifierController = AvesMagnifierController();
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
if (entry.isVideo) {
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
}
// idempotente
context.read<VideoConductor>().registerRemoteFactory(RemoteVideoControllerFactory());
viewerController.startAutopilotAnimation(
vsync: this,
onUpdate: ({required scaleLevel}) {
final boundaries = _magnifierController.scaleBoundaries;
if (boundaries != null) {
final scale = boundaries.scaleForLevel(scaleLevel);
_magnifierController.update(scale: scale, source: ChangeSource.animation);
}
},
);
}
void _unregisterWidget(EntryPageView oldWidget) {
viewerController.stopAutopilotAnimation(vsync: this);
_magnifierController.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Widget build(BuildContext context) {
Widget child = AnimatedBuilder(
animation: entry.visualChangeNotifier,
builder: (context, child) {
Widget? child;
// Pipeline unificata:
// - SVG → _buildSvgView()
// - Video → _buildVideoView()
// - Raster (locali + remoti) → _buildRasterView()
if (entry.isSvg) {
child = _buildSvgView();
} else if (!entry.displaySize.isEmpty) {
if (entry.isVideo) {
child = _buildVideoView();
} else if (entry.isDecodingSupported || _isRemote) {
child = _buildRasterView();
}
}
child ??= ErrorView(
entry: entry,
onTap: _onTap,
);
return child;
},
);
if (!settings.viewerUseCutout) {
child = SafeCutoutArea(
child: ClipRect(child: child),
);
}
final animate = context.select<Settings, bool>((v) => v.animate);
if (animate) {
child = Consumer<EntryHeroInfo?>(
builder: (context, info, child) => Hero(
tag: info != null && info.entry == mainEntry ? info.tag : hashCode,
transitionOnUserGestures: true,
child: child!,
),
child: child,
);
}
return child;
}
// RASTER (locali + remoti)
Widget _buildRasterView() {
return _buildMagnifier(
applyScale: false,
child: RasterImageView(
entry: entry,
viewStateNotifier: _viewStateNotifier,
errorBuilder: (context, error, stackTrace) => ErrorView(
entry: entry,
onTap: _onTap,
),
),
);
}
Widget _buildSvgView() {
return _buildMagnifier(
maxScale: EntryPageView.vectorMaxScale,
scaleStateCycle: _vectorScaleStateCycle,
applyScale: false,
child: VectorImageView(
entry: entry,
viewStateNotifier: _viewStateNotifier,
errorBuilder: (context, error, stackTrace) => ErrorView(
entry: entry,
onTap: _onTap,
),
),
);
}
Widget _buildVideoView() {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return const SizedBox();
return ValueListenableBuilder<double?>(
valueListenable: videoController.sarNotifier,
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
final isPureVideo = entry.isPureVideo;
return Selector<Settings, (bool, bool, bool)>(
selector: (context, s) => (
isPureVideo && s.videoGestureDoubleTapTogglePlay,
isPureVideo && s.videoGestureSideDoubleTapSeek,
isPureVideo && s.videoGestureVerticalDragBrightnessVolume,
),
builder: (context, s, child) {
final (playGesture, seekGesture, useVerticalDragGesture) = s;
final useTapGesture = playGesture || seekGesture;
MagnifierDoubleTapCallback? onDoubleTap;
MagnifierGestureScaleStartCallback? onScaleStart;
MagnifierGestureScaleUpdateCallback? onScaleUpdate;
MagnifierGestureScaleEndCallback? onScaleEnd;
if (useTapGesture) {
void _applyAction(EntryAction action, {IconData? Function()? icon}) {
_actionFeedbackChildNotifier.value = DecoratedIcon(
icon?.call() ?? action.getIconData(),
size: 48,
color: Colors.white,
shadows: const [Shadow(color: Colors.black, blurRadius: 4)],
);
VideoActionNotification(
controller: videoController,
entry: entry,
action: action,
).dispatch(context);
}
onDoubleTap = (alignment) {
final x = alignment.x;
if (seekGesture) {
final sideRatio = _getSideRatio();
if (sideRatio != null) {
if (x < sideRatio) {
_applyAction(EntryAction.videoReplay10);
return true;
} else if (x > 1 - sideRatio) {
_applyAction(EntryAction.videoSkip10);
return true;
}
}
}
if (playGesture) {
_applyAction(
EntryAction.videoTogglePlay,
icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
);
return true;
}
return false;
};
}
if (useVerticalDragGesture) {
SwipeAction? swipeAction;
var move = Offset.zero;
var dropped = false;
double? startValue;
ValueNotifier<double?>? valueNotifier;
onScaleStart = (details, doubleTap, boundaries) {
dropped = details.pointerCount > 1 || doubleTap;
if (dropped) return;
startValue = null;
valueNotifier = ValueNotifier<double?>(null);
final alignmentX = details.focalPoint.dx / boundaries.viewportSize.width;
final action = alignmentX > .5 ? SwipeAction.volume : SwipeAction.brightness;
action.get().then((v) => startValue = v);
swipeAction = action;
move = Offset.zero;
_actionFeedbackOverlayEntry = OverlayEntry(
builder: (context) => SwipeActionFeedback(
action: action,
valueNotifier: valueNotifier!,
),
);
Overlay.of(context).insert(_actionFeedbackOverlayEntry!);
};
onScaleUpdate = (details) {
if (valueNotifier == null) return false;
move += details.focalPointDelta;
dropped |= details.pointerCount > 1;
if (valueNotifier!.value == null) {
dropped |= MagnifierGestureRecognizer.isXPan(move);
}
if (dropped) return false;
final _startValue = startValue;
if (_startValue != null) {
final double value = (_startValue - move.dy / SwipeActionFeedback.height).clamp(0, 1);
valueNotifier!.value = value;
swipeAction?.set(value);
}
return true;
};
onScaleEnd = (details) {
valueNotifier?.dispose();
_actionFeedbackOverlayEntry
?..remove()
..dispose();
_actionFeedbackOverlayEntry = null;
};
}
Widget videoChild = Stack(
children: [
_buildMagnifier(
displaySize: videoDisplaySize,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onDoubleTap: onDoubleTap,
child: VideoView(
entry: entry,
controller: videoController,
),
),
VideoSubtitles(
entry: entry,
controller: videoController,
viewStateNotifier: _viewStateNotifier,
),
if (useTapGesture)
ValueListenableBuilder<Widget?>(
valueListenable: _actionFeedbackChildNotifier,
builder: (context, feedbackChild, child) => ActionFeedback(
child: feedbackChild,
),
),
],
);
if (useVerticalDragGesture) {
final scope = MagnifierGestureDetectorScope.maybeOf(context);
if (scope != null) {
videoChild = scope.copyWith(
acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
child: videoChild,
);
}
}
return Stack(
fit: StackFit.expand,
children: [
videoChild,
VideoCover(
mainEntry: mainEntry,
pageEntry: entry,
magnifierController: _magnifierController,
videoController: videoController,
videoDisplaySize: videoDisplaySize,
onTap: _onTap,
magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier(
controller: coverController,
displaySize: coverSize,
onDoubleTap: onDoubleTap,
child: Image(image: videoCoverUriImage),
),
),
],
);
},
);
},
);
}
/// Wrapper del Magnifier con bound **remotefriendly**
Widget _buildMagnifier({
AvesMagnifierController? controller,
Size? displaySize,
ScaleLevel maxScale = EntryPageView.rasterMaxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true,
MagnifierGestureScaleStartCallback? onScaleStart,
MagnifierGestureScaleUpdateCallback? onScaleUpdate,
MagnifierGestureScaleEndCallback? onScaleEnd,
MagnifierDoubleTapCallback? onDoubleTap,
required Widget child,
}) {
final isWallpaperMode = context.read<ValueNotifier<AppMode>>().value == AppMode.setWallpaper;
const contained = ScaleLevel(ref: ScaleReference.contained);
const covered = ScaleLevel(ref: ScaleReference.covered);
// REMOTO:
// - minScale sotto al fit per abilitare pinchout
// - initialScale al fit (poi RasterImageView lo “cappa” a <= 1.0)
final minScale = isWallpaperMode
? covered
: (_isRemote ? const ScaleLevel(ref: ScaleReference.contained, factor: .5) : contained);
final initialScale = _isRemote ? contained : viewerController.initialScale;
return ValueListenableBuilder<bool>(
valueListenable: AvesApp.canGestureToOtherApps,
builder: (context, canGestureToOtherApps, childWidget) {
return AvesMagnifier(
key: Key('${entry.uri}_${entry.pageId}_${entry.dateModifiedMillis}'),
controller: controller ?? _magnifierController,
contentSize: displaySize ?? entry.displaySize,
allowOriginalScaleBeyondRange: !isWallpaperMode,
allowDoubleTap: _allowDoubleTap,
minScale: minScale,
maxScale: maxScale,
initialScale: initialScale,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
onFling: _onFling,
onTap: (context, _, alignment, _) {
if (context.mounted) {
_onTap(alignment: alignment);
}
},
onLongPress: canGestureToOtherApps ? _startGlobalDrag : null,
onDoubleTap: onDoubleTap,
child: childWidget!,
);
},
child: child,
);
}
Future<void> _startGlobalDrag() async {
const dragShadowSize = Size.square(128);
final cornerRadiusPx = await deviceService.getWidgetCornerRadiusPx();
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
final brightness = Theme.of(context).brightness;
final outline = await WidgetOutline.systemBlackAndWhite.color(brightness);
final dragShadowBytes = await HomeWidgetPainter(
entry: entry,
devicePixelRatio: devicePixelRatio,
).drawWidget(
sizeDip: dragShadowSize,
cornerRadiusPx: cornerRadiusPx,
outline: outline,
shape: WidgetShape.rrect,
);
await windowService.startGlobalDrag(entry.uri, entry.bestTitle, dragShadowSize, dragShadowBytes);
}
void _onFling(AxisDirection direction) {
const animate = true;
switch (direction) {
case AxisDirection.left:
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.right:
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
case AxisDirection.up:
PopVisualNotification().dispatch(context);
case AxisDirection.down:
ShowInfoPageNotification().dispatch(context);
}
}
Notification? _handleSideSingleTap(Alignment? alignment) {
if (settings.viewerGestureSideTapNext && alignment != null) {
final x = alignment.x;
final sideRatio = _getSideRatio();
if (sideRatio != null) {
const animate = false;
if (x < sideRatio) {
return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate);
} else if (x > 1 - sideRatio) {
return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate);
}
}
}
return null;
}
void _onTap({Alignment? alignment}) => (_handleSideSingleTap(alignment) ?? const ToggleOverlayNotification()).dispatch(context);
bool _allowDoubleTap(Alignment alignment) {
if (entry.isVideo && settings.videoGestureSideDoubleTapSeek) {
return true;
}
final actionNotification = _handleSideSingleTap(alignment);
return actionNotification == null;
}
void _onMediaCommand(MediaCommandEvent event) {
final videoController = context.read<VideoConductor>().getController(entry);
if (videoController == null) return;
switch (event.command) {
case MediaCommand.play:
videoController.play();
case MediaCommand.pause:
videoController.pause();
case MediaCommand.skipToNext:
ShowNextVideoNotification().dispatch(context);
case MediaCommand.skipToPrevious:
ShowPreviousVideoNotification().dispatch(context);
case MediaCommand.stop:
videoController.pause();
case MediaCommand.seek:
if (event is MediaSeekCommandEvent) {
videoController.seekTo(event.position);
}
}
}
void _onViewStateChanged(MagnifierState v) {
if (!mounted) return;
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
position: v.position,
scale: v.scale,
);
}
void _onViewScaleBoundariesChanged(ScaleBoundaries v) {
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(
viewportSize: v.viewportSize,
contentSize: v.contentSize,
);
}
double? _getSideRatio() {
if (!mounted) return null;
final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait;
return isPortrait ? 1 / 6 : 1 / 8;
}
static ScaleState _vectorScaleStateCycle(ScaleState actual) {
switch (actual) {
case ScaleState.initial:
return ScaleState.covering;
default:
return ScaleState.initial;
}
}
}

View file

@ -1,3 +1,4 @@
// lib/widgets/viewer/visual/raster.dart
import 'dart:math';
import 'package:aves/image_providers/region_provider.dart';
@ -55,28 +56,33 @@ class _RasterImageViewState extends State<RasterImageView> {
ImageProvider get thumbnailProvider => entry.bestCachedThumbnail;
bool get _isRemote => entry.origin == 1;
ImageProvider get fullImageProvider {
if (entry.isRemote) {
return NetworkImage(RemoteHttp.absUrl(entry.remotePath!));
}
// === FULL IMAGE: provider sincrono (necessario per il listener)
ImageProvider get fullImageProvider {
if (_isRemote) {
final abs = RemoteHttp.absUrl(entry.remotePath ?? entry.path);
final hdrs = RemoteHttp.peekHeaders();
return NetworkImage(abs, headers: hdrs.isEmpty ? null : hdrs);
}
if (_useTiles) {
assert(_isTilingInitialized);
return entry.getRegion(
sampleSize: _maxSampleSize,
region: entry.fullImageRegion,
);
} else {
return entry.fullImage;
if (_useTiles) {
assert(_isTilingInitialized);
return entry.getRegion(
sampleSize: _maxSampleSize,
region: entry.fullImageRegion,
);
} else {
return entry.fullImage;
}
}
}
@override
void initState() {
super.initState();
_displaySize = entry.displaySize;
_useTiles = entry.isRemote ? false : entry.useTiles;
// REMOTO: disabilita tiling per i remoti
_useTiles = _isRemote ? false : entry.useTiles;
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
if (!_useTiles) _registerFullImage();
}
@ -110,6 +116,7 @@ ImageProvider get fullImageProvider {
_fullImageStream?.removeListener(_fullImageListener);
_fullImageStream = null;
_fullImageInfo?.dispose();
_fullImageInfo = null;
}
void _onFullImageCompleted(ImageInfo image, bool synchronousCall) {
@ -117,9 +124,16 @@ ImageProvider get fullImageProvider {
_unregisterFullImage();
_fullImageInfo = image;
_fullImageLoaded.value = true;
// REMOTO: NON aggiornare _displaySize quando arriva il full.
// Manteniamo quella di entry.displaySize (remoteWidth/remoteHeight),
// così il Magnifier non ricompone i limiti e non c'è "shrink".
FullImageLoadedNotification(entry, fullImageProvider).dispatch(context);
}
// REMOTO: non applicare mai scala > 1.0 (evita partenza zoomata)
double _effectiveScale(double raw) => _isRemote ? min(raw, 1.0) : raw;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ViewState>(
@ -129,15 +143,18 @@ ImageProvider get fullImageProvider {
final viewportSized = viewportSize?.isEmpty == false;
if (viewportSized && _useTiles && !_isTilingInitialized) _initTiling(viewportSize!);
final magnifierScale = viewState.scale!;
final magnifierScale = _effectiveScale(viewState.scale!);
final sized = _displaySize * magnifierScale;
return SizedBox.fromSize(
size: _displaySize * magnifierScale,
// evita 0×0 che può portare a overflow numerici in fase di layout
size: Size(max(1, sized.width), max(1, sized.height)),
child: Stack(
alignment: Alignment.center,
children: [
if (entry.canHaveAlpha && viewportSized) _buildBackground(),
_buildLoading(),
if (_useTiles) ..._buildTiles() else _buildFullImage(),
if (entry.canHaveAlpha && viewportSized) _buildBackground(magnifierScale),
_buildLoading(), // placeholder (anche remoto)
if (_useTiles) ..._buildTiles() else _buildFullImage(magnifierScale),
],
),
);
@ -145,22 +162,90 @@ ImageProvider get fullImageProvider {
);
}
Widget _buildFullImage() {
final magnifierScale = viewState.scale!;
// === FULL IMAGE (widget) ===
Widget _buildFullImage(double magnifierScale) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final quality = _qualityForScaleAndSize(
magnifierScale: magnifierScale,
sampleSize: 1,
devicePixelRatio: devicePixelRatio,
);
return Image(
Widget img = Image(
image: fullImageProvider,
gaplessPlayback: true,
errorBuilder: widget.errorBuilder,
width: (_displaySize * magnifierScale).width,
fit: BoxFit.contain,
fit: BoxFit.contain, // naturale: niente crop
filterQuality: quality,
);
// EXIF solo per locali (per i remoti è neutralizzato in entry.dart)
if (!_isRemote) {
if (entry.isFlipped) {
final rotated = (entry.rotationDegrees ~/ 90) % 2 != 0;
final w = (rotated ? (_displaySize.height * magnifierScale) : (_displaySize.width * magnifierScale)) / 2.0;
final h = (rotated ? (_displaySize.width * magnifierScale) : (_displaySize.height * magnifierScale)) / 2.0;
final flipper = Matrix4.identity()
..translateByDouble(w, h, 0, 1)
..scaleByDouble(-1.0, 1.0, 1.0, 1.0)
..translateByDouble(-w, -h, 0, 1);
img = Transform(transform: flipper, child: img);
}
final quarterTurns = (entry.rotationDegrees ~/ 90) % 4;
if (quarterTurns != 0) {
img = RotatedBox(quarterTurns: quarterTurns, child: img);
}
}
return img;
}
// === LOADING / PLACEHOLDER ===
Widget _buildLoading() {
return ValueListenableBuilder<bool>(
valueListenable: _fullImageLoaded,
builder: (context, fullImageLoaded, child) {
if (fullImageLoaded) return const SizedBox();
// Per i remoti, mostra thumb remoto (o path) via HTTP + header async
if (_isRemote) {
final rel = entry.remoteThumb2 ?? entry.remoteThumb1 ?? entry.remotePath ?? entry.path;
if (rel != null && rel.isNotEmpty) {
final url = RemoteHttp.absUrl(rel);
return AspectRatio(
aspectRatio: entry.displayAspectRatio,
child: FutureBuilder<Map<String, String>>(
future: RemoteHttp.headers(),
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const ColoredBox(color: Colors.black12);
}
final hdrs = snap.data ?? const {};
return Image.network(
url,
fit: BoxFit.contain, // evita fill (effetto zoomato) in loading
headers: hdrs.isEmpty ? null : hdrs,
);
},
),
);
}
}
// Default: usa il thumbnail migliore in cache locale
return Center(
child: AspectRatio(
// mantieni l'aspect ratio originale
aspectRatio: entry.displayAspectRatio,
child: Image(
image: thumbnailProvider,
fit: BoxFit.contain,
),
),
);
},
);
}
void _initTiling(Size viewportSize) {
@ -168,7 +253,10 @@ ImageProvider get fullImageProvider {
_tileSide = viewportSize.shortestSide * devicePixelRatio / _tilesByShortestSide;
// scale for initial state `contained`
final containedScale = min(viewportSize.width / _displaySize.width, viewportSize.height / _displaySize.height);
_maxSampleSize = ExtraAvesEntryImages.sampleSizeForScale(magnifierScale: containedScale, devicePixelRatio: devicePixelRatio);
_maxSampleSize = ExtraAvesEntryImages.sampleSizeForScale(
magnifierScale: containedScale,
devicePixelRatio: devicePixelRatio,
);
final rotationDegrees = entry.rotationDegrees;
final isFlipped = entry.isFlipped;
@ -184,31 +272,11 @@ ImageProvider get fullImageProvider {
_registerFullImage();
}
Widget _buildLoading() {
return ValueListenableBuilder<bool>(
valueListenable: _fullImageLoaded,
builder: (context, fullImageLoaded, child) {
if (fullImageLoaded) return const SizedBox();
return Center(
child: AspectRatio(
// enforce original aspect ratio, as some thumbnails aspect ratios slightly differ
aspectRatio: entry.displayAspectRatio,
child: Image(
image: thumbnailProvider,
fit: BoxFit.fill,
),
),
);
},
);
}
Widget _buildBackground() {
Widget _buildBackground(double magnifierScale) {
final viewportSize = viewState.viewportSize!;
final viewSize = _displaySize * viewState.scale!;
final viewSize = _displaySize * magnifierScale;
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
// deflate as a quick way to prevent background bleed
// deflate come quick way per evitare bleed
final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - const Offset(.5, .5)) as Size;
Widget child;
@ -255,8 +323,7 @@ ImageProvider get fullImageProvider {
final magnifierScale = viewState.scale!;
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
// for the largest sample size (matching the initial scale), the whole image is in view
// so we subsample the whole image without tiling
// per la sample size massima (scala iniziale), lintera immagine è in view
final fullImageRegionTile = _RegionTile(
entry: entry,
tileRect: Rect.fromLTWH(0, 0, displayWidth * magnifierScale, displayHeight * magnifierScale),
@ -270,7 +337,14 @@ ImageProvider get fullImageProvider {
);
final tiles = [fullImageRegionTile];
final minSampleSize = min(ExtraAvesEntryImages.sampleSizeForScale(magnifierScale: magnifierScale, devicePixelRatio: devicePixelRatio), _maxSampleSize);
final minSampleSize = min(
ExtraAvesEntryImages.sampleSizeForScale(
magnifierScale: magnifierScale,
devicePixelRatio: devicePixelRatio,
),
_maxSampleSize,
);
int nextSampleSize(int sampleSize) => (sampleSize / 2).floor();
for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) {
final regionSide = (_tileSide * sampleSize).round();
@ -432,7 +506,10 @@ class _RegionTileState extends State<_RegionTile> {
@override
void didUpdateWidget(covariant _RegionTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != widget.entry || oldWidget.tileRect != widget.tileRect || oldWidget.sampleSize != widget.sampleSize || oldWidget.sampleSize != widget.sampleSize) {
if (oldWidget.entry != widget.entry ||
oldWidget.tileRect != widget.tileRect ||
oldWidget.sampleSize != widget.sampleSize ||
oldWidget.sampleSize != widget.sampleSize) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@ -473,7 +550,7 @@ class _RegionTileState extends State<_RegionTile> {
filterQuality: widget.quality,
);
// apply EXIF orientation
// apply EXIF orientation (tile path locale)
final quarterTurns = entry.rotationDegrees ~/ 90;
if (entry.isFlipped) {
final rotated = quarterTurns % 2 != 0;
@ -502,7 +579,7 @@ class _RegionTileState extends State<_RegionTile> {
Positioned.fill(child: child),
Text(
'\ntile=(${tileRect.left.round()}, ${tileRect.top.round()}) ${tileRect.width.round()} x ${tileRect.height.round()}'
'\nregion=(${regionRect.left.round()}, ${regionRect.top.round()}) ${regionRect.width.round()} x ${regionRect.height.round()}'
'\nregion=(${regionRect.left.round()},{${regionRect.top.round()}) ${regionRect.width.round()} x ${regionRect.height.round()}'
'\nsampling=${widget.sampleSize} quality=${widget.quality.name}',
style: const TextStyle(backgroundColor: Colors.black87),
),

View file

@ -0,0 +1,602 @@
// lib/widgets/viewer/visual/raster.dart
import 'dart:math';
import 'package:aves/image_providers/region_provider.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/images.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/entry_background.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/viewer/view_state.dart';
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:aves/remote/remote_http.dart';
class RasterImageView extends StatefulWidget {
final AvesEntry entry;
final ValueNotifier<ViewState> viewStateNotifier;
final ImageErrorWidgetBuilder errorBuilder;
const RasterImageView({
super.key,
required this.entry,
required this.viewStateNotifier,
required this.errorBuilder,
});
@override
State<RasterImageView> createState() => _RasterImageViewState();
}
class _RasterImageViewState extends State<RasterImageView> {
late Size _displaySize;
late bool _useTiles;
bool _isTilingInitialized = false;
late int _maxSampleSize;
late double _tileSide;
Matrix4? _tileTransform;
ImageStream? _fullImageStream;
late ImageStreamListener _fullImageListener;
final ValueNotifier<bool> _fullImageLoaded = ValueNotifier(false);
ImageInfo? _fullImageInfo;
static const int _pixelArtMaxSize = 256; // px
static const double _tilesByShortestSide = 2;
AvesEntry get entry => widget.entry;
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifier;
ViewState get viewState => viewStateNotifier.value;
ImageProvider get thumbnailProvider => entry.bestCachedThumbnail;
bool get _isRemote => entry.origin == 1;
// === FULL IMAGE: provider sincrono (necessario per il listener)
ImageProvider get fullImageProvider {
if (_isRemote) {
final abs = RemoteHttp.absUrl(entry.remotePath ?? entry.path);
final hdrs = RemoteHttp.peekHeaders();
return NetworkImage(abs, headers: hdrs.isEmpty ? null : hdrs);
}
if (_useTiles) {
assert(_isTilingInitialized);
return entry.getRegion(
sampleSize: _maxSampleSize,
region: entry.fullImageRegion,
);
} else {
return entry.fullImage;
}
}
@override
void initState() {
super.initState();
_displaySize = entry.displaySize;
// REMOTO: disabilita tiling per i remoti
_useTiles = _isRemote ? false : entry.useTiles;
_fullImageListener = ImageStreamListener(_onFullImageCompleted);
if (!_useTiles) _registerFullImage();
}
@override
void didUpdateWidget(covariant RasterImageView oldWidget) {
super.didUpdateWidget(oldWidget);
final oldViewState = oldWidget.viewStateNotifier.value;
final viewState = widget.viewStateNotifier.value;
if (oldWidget.entry != widget.entry || oldViewState.viewportSize != viewState.viewportSize) {
_isTilingInitialized = false;
_fullImageLoaded.value = false;
_unregisterFullImage();
}
}
@override
void dispose() {
_fullImageLoaded.dispose();
_unregisterFullImage();
super.dispose();
}
void _registerFullImage() {
_fullImageStream = fullImageProvider.resolve(ImageConfiguration.empty);
_fullImageStream!.addListener(_fullImageListener);
}
void _unregisterFullImage() {
_fullImageStream?.removeListener(_fullImageListener);
_fullImageStream = null;
_fullImageInfo?.dispose();
_fullImageInfo = null;
}
void _onFullImageCompleted(ImageInfo image, bool synchronousCall) {
// implementer is responsible for disposing the provided `ImageInfo`
_unregisterFullImage();
_fullImageInfo = image;
_fullImageLoaded.value = true;
// ⚠️ REMOTO: NON aggiornare _displaySize quando arriva il full.
// Manteniamo quella di entry.displaySize (remoteWidth/remoteHeight),
// così il Magnifier non ricompone i limiti e non c'è "shrink".
FullImageLoadedNotification(entry, fullImageProvider).dispatch(context);
}
// REMOTO: non applicare mai scala > 1.0 (evita “partenza zoomata”)
double _effectiveScale(double raw) => _isRemote ? min(raw, 1.0) : raw;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ViewState>(
valueListenable: viewStateNotifier,
builder: (context, viewState, child) {
final viewportSize = viewState.viewportSize;
final viewportSized = viewportSize?.isEmpty == false;
if (viewportSized && _useTiles && !_isTilingInitialized) _initTiling(viewportSize!);
final magnifierScale = _effectiveScale(viewState.scale!);
final sized = _displaySize * magnifierScale;
return SizedBox.fromSize(
// evita 0×0 che può portare a overflow numerici in fase di layout
size: Size(max(1, sized.width), max(1, sized.height)),
child: Stack(
alignment: Alignment.center,
children: [
if (entry.canHaveAlpha && viewportSized) _buildBackground(magnifierScale),
_buildLoading(), // placeholder (anche remoto)
if (_useTiles) ..._buildTiles() else _buildFullImage(magnifierScale),
],
),
);
},
);
}
// === FULL IMAGE (widget) ===
Widget _buildFullImage(double magnifierScale) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final quality = _qualityForScaleAndSize(
magnifierScale: magnifierScale,
sampleSize: 1,
devicePixelRatio: devicePixelRatio,
);
Widget img = Image(
image: fullImageProvider,
gaplessPlayback: true,
errorBuilder: widget.errorBuilder,
width: (_displaySize * magnifierScale).width,
fit: BoxFit.contain, // naturale: niente crop
filterQuality: quality,
);
// EXIF solo per locali (per i remoti è neutralizzato in entry.dart)
if (!_isRemote) {
if (entry.isFlipped) {
final rotated = (entry.rotationDegrees ~/ 90) % 2 != 0;
final w = (rotated ? (_displaySize.height * magnifierScale) : (_displaySize.width * magnifierScale)) / 2.0;
final h = (rotated ? (_displaySize.width * magnifierScale) : (_displaySize.height * magnifierScale)) / 2.0;
final flipper = Matrix4.identity()
..translateByDouble(w, h, 0, 1)
..scaleByDouble(-1.0, 1.0, 1.0, 1.0)
..translateByDouble(-w, -h, 0, 1);
img = Transform(transform: flipper, child: img);
}
final quarterTurns = (entry.rotationDegrees ~/ 90) % 4;
if (quarterTurns != 0) {
img = RotatedBox(quarterTurns: quarterTurns, child: img);
}
}
return img;
}
// === LOADING / PLACEHOLDER ===
Widget _buildLoading() {
return ValueListenableBuilder<bool>(
valueListenable: _fullImageLoaded,
builder: (context, fullImageLoaded, child) {
if (fullImageLoaded) return const SizedBox();
// Per i remoti, mostra thumb remoto (o path) via HTTP + header async
if (_isRemote) {
final rel = entry.remoteThumb2 ?? entry.remoteThumb1 ?? entry.remotePath ?? entry.path;
if (rel != null && rel.isNotEmpty) {
final url = RemoteHttp.absUrl(rel);
return AspectRatio(
aspectRatio: entry.displayAspectRatio,
child: FutureBuilder<Map<String, String>>(
future: RemoteHttp.headers(),
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const ColoredBox(color: Colors.black12);
}
final hdrs = snap.data ?? const {};
return Image.network(
url,
fit: BoxFit.contain, // evita “fill” (effetto zoomato) in loading
headers: hdrs.isEmpty ? null : hdrs,
);
},
),
);
}
}
// Default: usa il thumbnail “migliore” in cache locale
return Center(
child: AspectRatio(
// mantieni l'aspect ratio originale
aspectRatio: entry.displayAspectRatio,
child: Image(
image: thumbnailProvider,
fit: BoxFit.contain,
),
),
);
},
);
}
void _initTiling(Size viewportSize) {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
_tileSide = viewportSize.shortestSide * devicePixelRatio / _tilesByShortestSide;
// scale for initial state `contained`
final containedScale = min(viewportSize.width / _displaySize.width, viewportSize.height / _displaySize.height);
_maxSampleSize = ExtraAvesEntryImages.sampleSizeForScale(
magnifierScale: containedScale,
devicePixelRatio: devicePixelRatio,
);
final rotationDegrees = entry.rotationDegrees;
final isFlipped = entry.isFlipped;
_tileTransform = null;
if (rotationDegrees != 0 || isFlipped) {
_tileTransform = Matrix4.identity()
..translateByDouble(entry.width / 2.0, entry.height / 2.0, 0, 1)
..scaleByDouble(isFlipped ? -1.0 : 1.0, 1.0, 1.0, 1.0)
..rotateZ(-degToRadian(rotationDegrees.toDouble()))
..translateByDouble(-_displaySize.width / 2.0, -_displaySize.height / 2.0, 0, 1);
}
_isTilingInitialized = true;
_registerFullImage();
}
Widget _buildBackground(double magnifierScale) {
final viewportSize = viewState.viewportSize!;
final viewSize = _displaySize * magnifierScale;
final decorationOffset = ((viewSize - viewportSize) as Offset) / 2 - viewState.position;
// deflate come “quick way” per evitare bleed
final decorationSize = (applyBoxFit(BoxFit.none, viewSize, viewportSize).source - const Offset(.5, .5)) as Size;
Widget child;
final background = settings.imageBackground;
if (background == EntryBackground.checkered) {
final side = viewportSize.shortestSide;
final checkSize = side / ((side / EntryPageView.decorationCheckSize).round());
final offset = ((decorationSize - viewportSize) as Offset) / 2;
child = ValueListenableBuilder<bool>(
valueListenable: _fullImageLoaded,
builder: (context, fullImageLoaded, child) {
if (!fullImageLoaded) return const SizedBox();
return CustomPaint(
painter: CheckeredPainter(
checkSize: checkSize,
offset: offset,
),
);
},
);
} else {
child = DecoratedBox(
decoration: BoxDecoration(
color: background.color,
),
);
}
return Positioned(
left: decorationOffset.dx >= 0 ? decorationOffset.dx : null,
top: decorationOffset.dy >= 0 ? decorationOffset.dy : null,
width: decorationSize.width,
height: decorationSize.height,
child: child,
);
}
List<Widget> _buildTiles() {
if (!_isTilingInitialized) return [];
final displayWidth = _displaySize.width.round();
final displayHeight = _displaySize.height.round();
final viewRect = _getViewRect(displayWidth, displayHeight);
final magnifierScale = viewState.scale!;
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
// per la sample size massima (scala iniziale), lintera immagine è in view
final fullImageRegionTile = _RegionTile(
entry: entry,
tileRect: Rect.fromLTWH(0, 0, displayWidth * magnifierScale, displayHeight * magnifierScale),
regionRect: entry.fullImageRegion,
sampleSize: _maxSampleSize,
quality: _qualityForScaleAndSize(
magnifierScale: magnifierScale,
sampleSize: _maxSampleSize,
devicePixelRatio: devicePixelRatio,
),
);
final tiles = [fullImageRegionTile];
final minSampleSize = min(
ExtraAvesEntryImages.sampleSizeForScale(
magnifierScale: magnifierScale,
devicePixelRatio: devicePixelRatio,
),
_maxSampleSize,
);
int nextSampleSize(int sampleSize) => (sampleSize / 2).floor();
for (var sampleSize = nextSampleSize(_maxSampleSize); sampleSize >= minSampleSize; sampleSize = nextSampleSize(sampleSize)) {
final regionSide = (_tileSide * sampleSize).round();
for (var x = 0; x < displayWidth; x += regionSide) {
for (var y = 0; y < displayHeight; y += regionSide) {
final rects = _getTileRects(
x: x,
y: y,
regionSide: regionSide,
displayWidth: displayWidth,
displayHeight: displayHeight,
scale: magnifierScale,
viewRect: viewRect,
);
if (rects != null) {
final (tileRect, regionRect) = rects;
tiles.add(
_RegionTile(
entry: entry,
tileRect: tileRect,
regionRect: regionRect,
sampleSize: sampleSize,
quality: _qualityForScaleAndSize(
magnifierScale: magnifierScale,
sampleSize: sampleSize,
devicePixelRatio: devicePixelRatio,
),
),
);
}
}
}
}
return tiles;
}
Rect _getViewRect(int displayWidth, int displayHeight) {
final scale = viewState.scale!;
final centerOffset = viewState.position;
final viewportSize = viewState.viewportSize!;
final viewOrigin = Offset(
((displayWidth * scale - viewportSize.width) / 2 - centerOffset.dx),
((displayHeight * scale - viewportSize.height) / 2 - centerOffset.dy),
);
return viewOrigin & viewportSize;
}
(Rect tileRect, Rectangle<num> regionRect)? _getTileRects({
required int x,
required int y,
required int regionSide,
required int displayWidth,
required int displayHeight,
required double scale,
required Rect viewRect,
}) {
final nextX = x + regionSide;
final nextY = y + regionSide;
final thisRegionWidth = regionSide - (nextX >= displayWidth ? nextX - displayWidth : 0);
final thisRegionHeight = regionSide - (nextY >= displayHeight ? nextY - displayHeight : 0);
final tileRect = Rect.fromLTWH(x * scale, y * scale, thisRegionWidth * scale, thisRegionHeight * scale);
// only build visible tiles
if (!viewRect.overlaps(tileRect)) return null;
Rectangle<num> regionRect;
if (_tileTransform != null) {
// apply EXIF orientation
final regionRectDouble = Rect.fromLTWH(x.toDouble(), y.toDouble(), thisRegionWidth.toDouble(), thisRegionHeight.toDouble());
final tl = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.topLeft);
final br = MatrixUtils.transformPoint(_tileTransform!, regionRectDouble.bottomRight);
regionRect = Rectangle<double>.fromPoints(
Point<double>(tl.dx, tl.dy),
Point<double>(br.dx, br.dy),
);
} else {
regionRect = Rectangle<num>(x, y, thisRegionWidth, thisRegionHeight);
}
return (tileRect, regionRect);
}
// follow recommended thresholds from `FilterQuality` documentation
static FilterQuality _qualityForScale({
required double magnifierScale,
required int sampleSize,
required double devicePixelRatio,
}) {
final entryScale = magnifierScale * devicePixelRatio;
final renderingScale = entryScale * sampleSize;
if (renderingScale > 1) {
return renderingScale > 10 ? FilterQuality.high : FilterQuality.medium;
} else {
return renderingScale < .5 ? FilterQuality.medium : FilterQuality.high;
}
}
// usually follow recommendations, except for small images
// (like icons, pixel art, etc.) for which the "nearest neighbor" algorithm is used
FilterQuality _qualityForScaleAndSize({
required double magnifierScale,
required int sampleSize,
required double devicePixelRatio,
}) {
if (_displaySize.longestSide < _pixelArtMaxSize) {
return FilterQuality.none;
}
return _qualityForScale(
magnifierScale: magnifierScale,
sampleSize: sampleSize,
devicePixelRatio: devicePixelRatio,
);
}
}
class _RegionTile extends StatefulWidget {
final AvesEntry entry;
// `tileRect` uses Flutter view coordinates
// `regionRect` uses the raw image pixel coordinates
final Rect tileRect;
final Rectangle<num> regionRect;
final int sampleSize;
final FilterQuality quality;
const _RegionTile({
required this.entry,
required this.tileRect,
required this.regionRect,
required this.sampleSize,
required this.quality,
});
@override
State<_RegionTile> createState() => _RegionTileState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('id', entry.id));
properties.add(IntProperty('contentId', entry.contentId));
properties.add(DiagnosticsProperty<Rect>('tileRect', tileRect));
properties.add(DiagnosticsProperty<Rectangle<num>>('regionRect', regionRect));
properties.add(IntProperty('sampleSize', sampleSize));
}
}
class _RegionTileState extends State<_RegionTile> {
late RegionProvider _provider;
AvesEntry get entry => widget.entry;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant _RegionTile oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != widget.entry ||
oldWidget.tileRect != widget.tileRect ||
oldWidget.sampleSize != widget.sampleSize ||
oldWidget.sampleSize != widget.sampleSize) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(_RegionTile widget) {
_initProvider();
}
void _unregisterWidget(_RegionTile widget) {
_pauseProvider();
}
void _initProvider() {
_provider = entry.getRegion(
sampleSize: widget.sampleSize,
region: widget.regionRect,
);
}
void _pauseProvider() => _provider.pause();
@override
Widget build(BuildContext context) {
final tileRect = widget.tileRect;
Widget child = Image(
image: _provider,
width: tileRect.width,
height: tileRect.height,
fit: BoxFit.fill,
filterQuality: widget.quality,
);
// apply EXIF orientation (tile path locale)
final quarterTurns = entry.rotationDegrees ~/ 90;
if (entry.isFlipped) {
final rotated = quarterTurns % 2 != 0;
final w = (rotated ? tileRect.height : tileRect.width) / 2.0;
final h = (rotated ? tileRect.width : tileRect.height) / 2.0;
final flipper = Matrix4.identity()
..translateByDouble(w, h, 0, 1)
..scaleByDouble(-1.0, 1.0, 1.0, 1.0)
..translateByDouble(-w, -h, 0, 1);
child = Transform(
transform: flipper,
child: child,
);
}
if (quarterTurns != 0) {
child = RotatedBox(
quarterTurns: quarterTurns,
child: child,
);
}
if (settings.debugShowViewerTiles) {
final regionRect = widget.regionRect;
child = Stack(
children: [
Positioned.fill(child: child),
Text(
'\ntile=(${tileRect.left.round()}, ${tileRect.top.round()}) ${tileRect.width.round()} x ${tileRect.height.round()}'
'\nregion=(${regionRect.left.round()},{${regionRect.top.round()}) ${regionRect.width.round()} x ${regionRect.height.round()}'
'\nsampling=${widget.sampleSize} quality=${widget.quality.name}',
style: const TextStyle(backgroundColor: Colors.black87),
),
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.red, width: 1),
),
),
),
],
);
}
return Positioned.fromRect(
rect: tileRect,
child: child,
);
}
}

3
lines.txt Normal file
View file

@ -0,0 +1,3 @@
1577
1578
1579

1
ls.db Normal file
View file

@ -0,0 +1 @@
/data/user/0/deckers.thibault.aves.debug/databases/metadata.db

6957
metadata.csv Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
metadata_device.db Normal file

Binary file not shown.

16053
output.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -5,3 +5,6 @@ export 'src/settings/subtitles.dart';
export 'src/settings/video.dart';
export 'src/stream.dart';
export 'src/video_loop_mode.dart';
// NUOVO: controller per i remoti (wrappa video_player)
export 'src/remote_controller.dart';

View file

@ -0,0 +1,217 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:aves_model/aves_model.dart';
import 'package:aves_video/aves_video.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:aves/remote/remote_http.dart' as remote;
class RemoteNetworkVideoController extends AvesVideoController {
final String url;
Map<String, String>? httpHeaders;
late final VideoPlayerController _vp;
final _statusCtr = StreamController<VideoStatus>.broadcast();
final _positionCtr = StreamController<int>.broadcast();
final _speedCtr = StreamController<double>.broadcast();
final _volumeCtr = StreamController<double>.broadcast();
final ValueNotifier<bool> _canCaptureFrame = ValueNotifier(false);
final ValueNotifier<bool> _canMute = ValueNotifier(true);
final ValueNotifier<bool> _canSetSpeed = ValueNotifier(true);
final ValueNotifier<bool> _canSelectStream = ValueNotifier(false);
final ValueNotifier<double?> _sar = ValueNotifier<double?>(null);
final _playCompleted = ValueNotifier<bool>(false);
Timer? _posTimer;
double _speed = 1.0;
bool _muted = false;
RemoteNetworkVideoController(
AvesEntryBase entry, {
required this.url,
this.httpHeaders,
required super.playbackStateHandler,
required super.settings,
}) : super(entry) {
_init();
}
Future<void> _init() async {
try {
// headers: usa cache se presente, altrimenti fetch adesso
httpHeaders ??= remote.RemoteHttp.peekHeaders() ?? await remote.RemoteHttp.headers();
// networkUrl con headers NON nulli
_vp = VideoPlayerController.networkUrl(Uri.parse(url), httpHeaders: httpHeaders ?? const {});
// Se la tua versione del plugin non ha networkUrl(...), usa la riga sotto:
// _vp = VideoPlayerController.network(url, httpHeaders: httpHeaders ?? const {});
await _vp.initialize();
_statusCtr.add(VideoStatus.initialized);
_speed = _vp.value.playbackSpeed;
_speedCtr.add(_speed);
_muted = _vp.value.volume == 0;
_volumeCtr.add(_vp.value.volume);
// Poll leggero della posizione
_posTimer = Timer.periodic(const Duration(milliseconds: 250), (_) {
final v = _vp.value;
if (!v.isInitialized) return;
_positionCtr.add(v.position.inMilliseconds);
final d = v.duration;
if (d.inMilliseconds > 0 &&
(d - v.position).inMilliseconds <= 200 &&
!_playCompleted.value) {
_playCompleted.value = true;
_statusCtr.add(VideoStatus.completed);
}
});
} catch (_) {
_statusCtr.add(VideoStatus.error);
}
}
@override
Future<void> dispose() async {
_posTimer?.cancel();
await _vp.dispose();
_statusCtr.close();
_positionCtr.close();
_speedCtr.close();
_volumeCtr.close();
_canCaptureFrame.dispose();
_canMute.dispose();
_canSetSpeed.dispose();
_canSelectStream.dispose();
_sar.dispose();
_playCompleted.dispose();
await super.dispose();
}
@override
void onVisualChanged() {}
@override
Future<void> play() async {
await _vp.play();
_statusCtr.add(VideoStatus.playing);
}
@override
Future<void> pause() async {
await _vp.pause();
_statusCtr.add(VideoStatus.paused);
}
@override
Future<void> seekTo(int targetMillis) async {
await _vp.seekTo(Duration(milliseconds: targetMillis));
}
@override
Future<void> skipFrames(int frameCount) async {
// stima ~30fps
final delta = Duration(milliseconds: (1000 / 30 * frameCount).round());
await _vp.seekTo(_vp.value.position + delta);
}
@override
Listenable get playCompletedListenable => _playCompleted;
@override
VideoStatus get status {
final v = _vp.value;
if (!v.isInitialized) return VideoStatus.idle;
if (v.hasError) return VideoStatus.error;
return v.isPlaying ? VideoStatus.playing : VideoStatus.paused;
}
@override
Stream<VideoStatus> get statusStream => _statusCtr.stream;
@override
Stream<double> get volumeStream => _volumeCtr.stream;
@override
Stream<double> get speedStream => _speedCtr.stream;
@override
bool get isReady => _vp.value.isInitialized;
@override
int get duration => _vp.value.isInitialized ? _vp.value.duration.inMilliseconds : 0;
@override
int get currentPosition => _vp.value.position.inMilliseconds;
@override
Stream<int> get positionStream => _positionCtr.stream;
@override
Stream<String?> get timedTextStream => const Stream.empty();
@override
ValueNotifier<bool> get canCaptureFrameNotifier => _canCaptureFrame;
@override
ValueNotifier<bool> get canMuteNotifier => _canMute;
@override
ValueNotifier<bool> get canSetSpeedNotifier => _canSetSpeed;
@override
ValueNotifier<bool> get canSelectStreamNotifier => _canSelectStream;
@override
ValueNotifier<double?> get sarNotifier => _sar;
@override
bool get isMuted => _muted;
@override
double get speed => _speed;
@override
double get minSpeed => 0.25;
@override
double get maxSpeed => 2.0;
@override
set speed(double s) {
_speed = s.clamp(minSpeed, maxSpeed);
_vp.setPlaybackSpeed(_speed);
_speedCtr.add(_speed);
}
@override
Future<void> selectStream(MediaStreamType type, MediaStreamSummary? selected) async {
// non implementato in questa versione minima
}
@override
Future<MediaStreamSummary?> getSelectedStream(MediaStreamType type) async => null;
@override
List<MediaStreamSummary> get streams => const [];
@override
Future<Uint8List?> captureFrame() async => null;
@override
Future<void> mute(bool muted) async {
_muted = muted;
await _vp.setVolume(muted ? 0 : 1);
_volumeCtr.add(_vp.value.volume);
}
@override
Widget buildPlayerWidget(BuildContext context) {
if (!isReady) return const SizedBox();
final ar = _vp.value.aspectRatio == 0 ? 16 / 9 : _vp.value.aspectRatio;
return AspectRatio(aspectRatio: ar, child: VideoPlayer(_vp));
}
}

View file

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182
sha256: afe15ce18a287d2f89da95566e62892df339b1936bbe9b83587df45b944ee72a
url: "https://pub.dev"
source: hosted
version: "1.3.66"
version: "1.3.67"
analyzer:
dependency: transitive
description:
@ -29,10 +29,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.7"
version: "4.0.9"
args:
dependency: transitive
description:
@ -219,6 +219,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.12"
dchs_motion_sensors:
dependency: transitive
description:
name: dchs_motion_sensors
sha256: "2a6eec0c47fd59d0f923aa758286ded0d74ae79519dec560d003e6d0797877e2"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
decorated_icon:
dependency: "direct main"
description:
@ -320,10 +328,10 @@ packages:
dependency: transitive
description:
name: firebase_core
sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80"
sha256: f0997fee80fbb6d2c658c5b88ae87ba1f9506b5b37126db64fc2e75d8e977fbb
url: "https://pub.dev"
source: hosted
version: "4.4.0"
version: "4.5.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -336,26 +344,26 @@ packages:
dependency: transitive
description:
name: firebase_core_web
sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084"
sha256: "856ca92bf2d75a63761286ab8e791bda3a85184c2b641764433b619647acfca6"
url: "https://pub.dev"
source: hosted
version: "3.4.0"
version: "3.5.0"
firebase_crashlytics:
dependency: transitive
description:
name: firebase_crashlytics
sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b
sha256: "2a6dc88d762af01790a05ff0cf814f7d4020050e8c69dec01962d9ed5dc1a531"
url: "https://pub.dev"
source: hosted
version: "5.0.7"
version: "5.0.8"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d
sha256: "5fd59d76d691f370e42fd2b786d46078e69ed4126ca0d84b585119f55cd97937"
url: "https://pub.dev"
source: hosted
version: "3.8.17"
version: "3.8.18"
fixnum:
dependency: transitive
description:
@ -559,10 +567,10 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: "1d648d2dd2047d7f7450d5727ca24ee435f240385753d90b49650e3cdff32e56"
sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66"
url: "https://pub.dev"
source: hosted
version: "9.2.0"
version: "9.2.1"
glob:
dependency: transitive
description:
@ -615,26 +623,26 @@ packages:
dependency: transitive
description:
name: google_maps_flutter_android
sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e"
sha256: ba0947315ddc9107ecc8d95fa26eb3b87b4f27b221606ce72518314d99c7306c
url: "https://pub.dev"
source: hosted
version: "2.18.12"
version: "2.19.2"
google_maps_flutter_ios:
dependency: transitive
description:
name: google_maps_flutter_ios
sha256: "38f0a9ee858b0de3a5105e7efe200f154eea8397eb0c36bea6b3810429fbc0e4"
sha256: a2e3c7ad2392ea65d6775704716d0aa3c3d226cb984fd0a688bca40f6be1a451
url: "https://pub.dev"
source: hosted
version: "2.17.3"
version: "2.17.4"
google_maps_flutter_platform_interface:
dependency: transitive
description:
name: google_maps_flutter_platform_interface
sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd
sha256: "0f8c6674d70c7e9a09cd34f63b18ebaf8a5822e85b558128eae0fdf02b4a3e93"
url: "https://pub.dev"
source: hosted
version: "2.14.1"
version: "2.14.2"
google_maps_flutter_web:
dependency: transitive
description:
@ -783,18 +791,18 @@ packages:
dependency: "direct main"
description:
name: local_auth
sha256: a4f1bf57f0236a4aeb5e8f0ec180e197f4b112a3456baa6c1e73b546630b0422
sha256: ae6f382f638108c6becd134318d7c3f0a93875383a54010f61d7c97ac05d5137
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "162b8e177fd9978c4620da2a8002a5c6bed4d20f0c6daf5137e72e9a8b767d2e"
sha256: dc9663a7bc8ac33d7d988e63901974f63d527ebef260eabd19c479447cc9c911
url: "https://pub.dev"
source: hosted
version: "2.0.4"
version: "2.0.5"
local_auth_darwin:
dependency: transitive
description:
@ -923,15 +931,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
motion_sensors:
dependency: transitive
description:
path: "."
ref: aves
resolved-ref: "4b11d59f4bda152627f701070272f657f8358e67"
url: "https://github.com/deckerst/aves_panorama_motion_sensors.git"
source: git
version: "0.1.0"
nested:
dependency: transitive
description:
@ -1012,15 +1011,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.3+7"
panorama:
panorama_viewer:
dependency: "direct main"
description:
path: "."
ref: aves
resolved-ref: "56de42183e4c1df7b64a714d9066049b9febb85c"
url: "https://github.com/deckerst/aves_panorama.git"
source: git
version: "0.4.1"
name: panorama_viewer
sha256: c6817bb2e202aad2d143d0bf7f95fc200b87791c2286971d7e3f29f8be916482
url: "https://pub.dev"
source: hosted
version: "2.0.7"
path:
dependency: "direct main"
description:
@ -1209,10 +1207,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
version: "6.5.0"
printing:
dependency: "direct main"
description:
@ -1711,10 +1709,10 @@ packages:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.2"
version: "4.5.3"
vector_map_tiles:
dependency: "direct main"
description:
@ -1755,6 +1753,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: "9862c67c4661c98f30fe707bc1a4f97d6a0faa76784f485d282668e4651a7ac3"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4
url: "https://pub.dev"
source: hosted
version: "2.8.9"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.dev"
source: hosted
version: "6.6.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
@ -1767,10 +1805,10 @@ packages:
dependency: "direct main"
description:
name: volume_controller
sha256: "5c1a13d2ea99d2f6753e7c660d0d3fab541f36da3999cafeb17b66fe49759ad7"
sha256: "966ed51695bed77823b7ab429854b63e616e414c31ed171147553c9ca5cc9672"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.4.2"
wakelock_plus:
dependency: transitive
description:

View file

@ -104,16 +104,17 @@ dependencies:
overlay_support:
package_info_plus:
palette_generator: # discontinued on 2025/04/30
panorama:
git:
url: https://github.com/deckerst/aves_panorama.git
ref: aves
# panorama:
# git:
# url: https://github.com/deckerst/aves_panorama.git
# ref: aves
panorama_viewer: ^2.0.7
path:
pattern_lock:
pdf:
percent_indicator:
permission_handler:
pin_code_fields:
pin_code_fields: ^8.0.1
printing:
proj4dart:
provider:
@ -132,6 +133,7 @@ dependencies:
vector_map_tiles: ^9.0.0-beta
vector_math:
vector_tile_renderer:
video_player: ^2.10.1
volume_controller:
xml:
flutter_secure_storage: ^9.2.2

101
queued_ids.txt Normal file
View file

@ -0,0 +1,101 @@
b3cda38a2fdee7ba9bdd2ea07d936b2fa5361baa21be2b2c29f1ad8ebcc4c31e
09ef919ff2a105ad75d2e7631b9c02228c1784df5048c98276fd379f1533360b
dba1de0e187650e62118279beb2d83e8759a6d4fb8a5412a6a18cdc4e01a33d7
751d8a15766bf2686bc87e60b33b4a8c38befe454428e6a5a82b00f26026c0e0
73dfd75dd639da9c7c0d4a6244f189a833d543bfdd4059b06f9f98e49f83c937
f32173d43d16b47eda3e7a32257ec956287d60b0516a5f6e46a8d1d433f53fd8
90c0a0932fdfa3eefb64156dec5b366c4af5245def4cc619d8eee29ca72cd7fe
4d74430aa1fbc45154ad4e8bf43bff334065ebd2757367806d4b8c165c9a7935
1955c91d411219c131c210b97fe38eb78df83be6ca12cdc845b952bcb78abc6f
14616f6446b0ffc01895f24db1656fa0b7a33da3b9e8c8e24065647faa2138f2
7668b4a6cf960b548a0bd1480444a3a039a4ca59d16656d2a1a3ca80a11eaa41
4a10ae72b84c6e205c8295dc1f94b5407d78f25576707fba0f3bb9093e70ce1c
490631b5d03e797f7ca50b5e42be1cdabafd54b20804b51d2c9565aef18de750
1a9759607a60387b7ff383fce2fce32f5e5aba91c0b0d3e09c1c2763c40334be
486d095f85a006800b9d3760e2ef4ac7ba909d82ba1347800a273d99395962a3
69eb91132640feeb561e94bb3258ca69c41e27ca711cb814c2ac33270599167a
1da97cbc6e7914cbd33e4dbd51729cd45f1c77116c1f5be9531adeb6424eb6e2
7fbc8546424236f94107681a69ed37fa94159377cf378a44f9d0e907a2afd31a
35772196ab28b9fd957199f81b267c9b99097055bfdcf1749e0c772241681cac
48b26b6f9aaf835dd90e6098194dedfbee75b85c20429d939188e70873dd4274
10ca45bc5aab42b5ba08da7d6a15d7f942478ca5e052f89054097c93d3dd6134
c6b58236822fab393b8793af2991115182afaf13b26a27394c4d5d6a799d0969
1d4945502b8686c4f4dc3ea94bfb37db7587d505e6c411bdf11976402c000e90
bf2f60a20771c3647d605a7cc192b95a7e078b7c6530ad30462ee6a2794c9df0
7d753539885cd882a1fdf04241eda6bfb8332ba1ed32bfc078dbadb3e5eb321f
29eb6ed3cd8e3d85e0803d091291b75677e6de2702918a286047894e444d669b
ebaf024e4df7764c2c458484d6e6eef1ced76611dd70b84f82af46337ba13839
c782108a839231c6dda63924f0b855747f98a4f56d8e93390f70877d4a452db6
f56db1595a3b822e1e5cc119c101904285dca3f3f1b8edbf0bfa42999583cc2f
5d03b5d0866a5cce0b6a12b78173c1f77eef5b1e5bfc5a4fd8e2cfd1c53a2b8e
dd715de9995fd11a23fee1df7c1561ea45bd215e765e9e93b2efc3f2ca59c39c
8e1a2d2abbd36803ec8670f56869ae723c4c8c8fcd5e634ada9f988c942df1b8
46412728d6155abe53abfec76c97898aad60e36322a46f4a4da280fba8c7c0cc
999cc0997f51584fef2627c5c9bd87d2f41cd9fe52d930fdc85f791c6adc328e
7ed0d6366274f3cadfc4caa618847c9c6c250e6b55127367d4ad1e3ca8322436
4025ea19107cf40d092576e4aaa122bdfb027dc90592a50dd6fa8131838dae69
d91b81a39d72056b2b7eb5bbf6058cf5b5df85cd5d9ca7e2a1bc7e9b897ebfb4
49a2098503bd07db18c869b17419352a0a2232389b3cad98cbb013ad42882600
a856fcea4cade1ed9c59dab96f180c06f18bc97a39654301ad623aed4a4979d7
fd3df96d42b300c66332b98ce7639a79ae07aa0cbadf6ebbd283901bcca510c6
c8ada02d1e1b7155607e905de25546c0a079c39528fe6df0b601a9c7b5f5df93
7351a37b3092015b6dee6e2911581ce590043619497ca84240b71e5ee37a0d2d
23852fb024f46c21667b30107942c40d57429b37c0df1549ab32004a739d792f
0ed7a1b65e9181db517fc4b75dcb3c8f398b7c3cd880ebc6d345886d070dc843
64ec16ebc0611619d7c397453119ffe1f80f211b7bc9c428f65f8d761c6aa61f
8fa73c5ac88c878b12ec926377f54e4d01187d92cc150730c514284d34581b39
ddab26bedbe3287cb210d46a2397336f879655c99224f90f8da1646ee111fee8
a07cfd0dfca3d9377ea2f4fea5501c6403f90f4a42a5732bde8b5d31214a2a7a
7a9382798ebaa67e80804165044390a2fded193ec5aa9753e75b62d9d54ce39a
9e8eb4959ed23f9dc3f8722bca1c5dd5a13ff7a6b95a0f0bf8df430d0e2ee9c3
df20d33dc73c45e9832c4a83fa5e4fd625c58518dfab5e3465d55959539cf57a
f46da4f2e78a8428eda4ea3a0363878cd50dd1d4b3798e4c858a34a217cc53d6
68c6d643cbe16143e9255bd3c84576e943b60412d506c090f4a7b93de3e9fd56
6b4d4396b06a0513b53fa9d088c1a95bbfacb99ede015addb9e34ad973fd77f4
dc17f07d88bdd90f4e439911484a2a31f91acb15fcb1201115f16157eec70e66
4f041976518502b14f0520489ac932cfd3f9e4e9226e38b09b483327b34ad3bf
4a125c9271159191ab923bc14fb5398d08aef6ee5ba313a2f6b1599a52e4e0cc
0bd35f8c46fa2ec4979d977d13bab6b8e0dc18365a0f25a7d87d81c5336c0378
4065f86b9af59235a5727abaaccb54a9a91fa585b464da2c592db5ed300d8836
76ea19c9ae626658b75942ad3fb9cf4caae2b2a4b845103dac42c006a3af3984
065a80861c5452dd07586b786aad63e9d74a822cbf10913201658c54023e63cd
e30a1427708cfc8d63a66d369781b15ac6a07b43e13550b1a7625003914503ed
c584bb41bc73b6b452c6964622ea7e4ccfddc741ed6e1d3757ed6015b59cf65f
77b822134129d751070d73441dda90996dab21186c50be2ce79064e22bba94bd
be834d7663441134af0e4fc6619b1c3ae06b5ce0856beac123ac6972c5b270d7
6750d21dec9eb3436ce6c821b40e344ef0fb280e43081cbc5e584310711417b7
ea8ae33ca15f21e84d75f5b853a88ca78d722b4f461478bdccfa9c059f6b69db
3be962052af0da265a1bd9cb69ed6868798f47343a2e1dc774c77568c20139bf
3041f044b86b8888937659a1c691db34f0c8b52b6dec6a7db023c18e9cd24722
50c16b24a37a6d76da00bc64b0a6f96ae858ba386dd5750ce0187a7cc3402027
df606e3b1a55f9f2c90e333bd113a0af2beb83a319aac9619b5b9cd729b13b0e
2454c04dd9067d2b5288dcca99c5c3eac85660779a97d6165a08ca7c37481070
bc0155f7235dc0ede6921f43debb8ebe8ca44fb1db57447d26a63f6904f38bba
a99f57f0bea2cb9b6caf3d0e60c3719e379d3d5bc2bbe37d1044c764214dd0b9
915492fbf9fc1769985e4f5684d42728b5ee63092b148b887c6d07d1fb66fe8f
0c2e99aa7c89c4a124019241d0131a03877d49bef2e409be8421b93d0f15ed82
a80cdc2fe8bfed0bddc25931f17f5f263ea273cca34558d9e190effa5fcc7bc9
308808d70076dd738c2d8d7703332dd000fb5592084ff292e2e7d2e43cac75df
20bdd8d0c71b2e1b6a55586e2def4db18152371efbd966b98f055ba340bf471a
37df228c1e983e94e3abea8800b04660e647034fca4948ef45ed6ec69d468687
1a4313d5086018efcd689dbca06040d99a75f7b331798b0fa2ced0d25dea95b7
6ab20a650288559438d81881ade44773873472c54d4d8cbbe69aaf2337c9d92b
6cf111099d98f97b09cf7a397d9ef6c849693e8273bf6026e0af0f257c0240bc
e830691975e126ad19891ea10585409a551791e8d0e8c6eda7e9d5e7878ce6a2
9fabc411f90e2a1452f4880b21ee6cdfb14c1274ce8c7a9a23c30444f07b370b
3846bf7e160007e43c6de5a612b3384ad4235276b7ac86284fd2a7e45a3b0a27
b4fec8470cd605882c09ec97558d3a737d25a43488adfb903c3573f8b1fb5ebc
b864196e118aa2b40142438c2e28df587c53fb27e6d4e24fa9b98a00cecccdc9
b1f9d25d6d7217c01a3a67f59176d54ce73b1bb626a687d1447a5354e5cad020
e9549aa2bb21ad70ff35ad1626cf15f6cb62a321e25e3e7ab832bd9e7430e186
b8013a96d80b1a6c3b39384f0910ff1f4507fcbf6936b90472d066f75772e751
d32b08d40cee22365e0193a7372cf85e997501d1e5e6d67b80aaddd569ee7030
0402bf83d35731c94314d6333cea4350e725209307a45fe6fe548ae04bf2e763
10e4a1c655829f11ad8f465cfc1e105e7430b9c0bf36f5589624cd5932c50457
a570da3c838dfd9f3035b658b8d7e873baf6399cf5d39674b37fb4ae4961879e
5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72
283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1
11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1
87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968
f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488
e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1

BIN
rem.zip Normal file

Binary file not shown.

101
repo_rids_unique.txt Normal file
View file

@ -0,0 +1,101 @@
b3cda38a
09ef919f
dba1de0e
751d8a15
73dfd75d
f32173d4
90c0a093
4d74430a
1955c91d
14616f64
7668b4a6
4a10ae72
490631b5
1a975960
486d095f
69eb9113
1da97cbc
7fbc8546
35772196
48b26b6f
10ca45bc
c6b58236
1d494550
bf2f60a2
7d753539
29eb6ed3
ebaf024e
c782108a
f56db159
5d03b5d0
dd715de9
8e1a2d2a
46412728
999cc099
7ed0d636
4025ea19
d91b81a3
49a20985
a856fcea
fd3df96d
c8ada02d
7351a37b
23852fb0
0ed7a1b6
64ec16eb
8fa73c5a
ddab26be
a07cfd0d
7a938279
9e8eb495
df20d33d
f46da4f2
68c6d643
6b4d4396
dc17f07d
4f041976
4a125c92
0bd35f8c
4065f86b
76ea19c9
065a8086
e30a1427
c584bb41
77b82213
be834d76
6750d21d
ea8ae33c
3be96205
3041f044
50c16b24
df606e3b
2454c04d
bc0155f7
a99f57f0
915492fb
0c2e99aa
a80cdc2f
308808d7
20bdd8d0
37df228c
1a4313d5
6ab20a65
6cf11109
e8306919
9fabc411
3846bf7e
b4fec847
b864196e
b1f9d25d
e9549aa2
b8013a96
d32b08d4
0402bf83
10e4a1c6
a570da3c
5c59ae93
283c973a
11ce5c19
87f69b97
f084723d
e18affb3

66
rid_history.txt Normal file
View file

@ -0,0 +1,66 @@
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=5c59ae9398c232eaa2f9e472df32691eb55b335bf07eb4e873d374b4144dbc72
I/flutter ( 2891): [repo-upsert] in: rid=283c973a name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135543.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
D/OpenGLRenderer( 2891): --- Failed to create image decoder with message 'unimplemented'
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1
I/flutter ( 2891): [repo-upsert] in: rid=11ce5c19 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135641.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1
I/flutter ( 2891): [repo-upsert] in: rid=87f69b97 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135744.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 name=
I/flutter ( 2891): [repo-upsert] row remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968
I/flutter ( 2891): [repo-upsert] in: rid=f084723d name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135745.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 name=
I/flutter ( 2891): [repo-upsert] row remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488
----
I/flutter ( 2891): [repo-upsert] in: rid=283c973a name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135543.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
D/OpenGLRenderer( 2891): --- Failed to create image decoder with message 'unimplemented'
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1
I/flutter ( 2891): [repo-upsert] in: rid=11ce5c19 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135641.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1
I/flutter ( 2891): [repo-upsert] in: rid=87f69b97 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135744.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 name=
I/flutter ( 2891): [repo-upsert] row remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968
I/flutter ( 2891): [repo-upsert] in: rid=f084723d name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135745.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 name=
I/flutter ( 2891): [repo-upsert] row remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488
I/flutter ( 2891): [repo-upsert] in: rid=e18affb3 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4"
----
I/flutter ( 2891): [repo-upsert] about to insert remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
D/OpenGLRenderer( 2891): --- Failed to create image decoder with message 'unimplemented'
I/flutter ( 2891): AvesMethodChannel invokeMethod name=deckers.thibault/aves/analysis_service_background method=updateNotification arguments={title: Loading, message: null}
I/WM-Processor( 2891): Moving WorkSpec (03037187-348b-4f75-8601-aab27a012776) to the foreground
D/WM-SystemFgDispatcher( 2891): Notifying with (id:1, workSpecId: 03037187-348b-4f75-8601-aab27a012776, notificationType :1)
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=283c973acf706e13a30216305b18833107ddaa420824dca4411c8d00870907b1
I/flutter ( 2891): [repo-upsert] in: rid=11ce5c19 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135641.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 name=
I/flutter ( 2891): [repo-upsert] row remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=11ce5c198063ed0c30460e66e49a654560cb477e104f783f0f521bc363a21ce1
I/flutter ( 2891): [repo-upsert] in: rid=87f69b97 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135744.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 name=
I/flutter ( 2891): [repo-upsert] row remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=87f69b9734668eb0e7bd00cdb9b27b8f46d2105a77fbd750cb80cf14d5f14968
I/flutter ( 2891): [repo-upsert] in: rid=f084723d name= raw="/photos/Fabio/original/2017Irlanda19-29ago/IMG_20220619_135745.jpg"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 name=
I/flutter ( 2891): [repo-upsert] row remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488 keys=id,contentId,uri,path,sourceMimeType,width,height,sourceRotationDegrees,sizeBytes,title,dateAddedSecs,dateModifiedMillis,sourceDateTakenMillis,durationMillis,trashed,origin,provider,latitude,longitude,altitude,remoteId,remotePath,remoteThumb1,remoteThumb2,remoteRotation,remoteWidth,remoteHeight size=27
I/flutter ( 2891): [repo-upsert] batch.insert queued remoteId=f084723d77c75764a1d23f6ef625f569c1825b36d66b5b2b5df798924602b488
I/flutter ( 2891): [repo-upsert] in: rid=e18affb3 name= raw="/photos/Fabio/original/2017Irlanda19-29ago/VID_20260221_095917.mp4"
I/flutter ( 2891): [repo-upsert] about to insert remoteId=e18affb38f030e3bc631457f0ff2ecaf041b0e150d0e87dd8a23c7bb068ca7c1 name=
----

0
trash.csv Normal file
View file

0
vaults.csv Normal file
View file

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