ok con video e foto in galleria aves
This commit is contained in:
parent
4925c6e3eb
commit
084fa184da
101 changed files with 42959 additions and 5475 deletions
|
|
@ -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
2058
address.csv
Normal file
File diff suppressed because it is too large
Load diff
102
after.csv
Normal file
102
after.csv
Normal 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
|
||||
|
500
after_commit_500.txt
Normal file
500
after_commit_500.txt
Normal 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
2
android_metadata.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
locale
|
||||
it_IT
|
||||
|
1
apk.sh
Normal file
1
apk.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
fvm flutter build apk --debug -t lib/main_play.dart --flavor play
|
||||
102
before.csv
Normal file
102
before.csv
Normal 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
before.sha1
Normal file
1
before.sha1
Normal file
|
|
@ -0,0 +1 @@
|
|||
d293b0e55340b249a24532d2dd61a6ebcfb328fb metadata.db
|
||||
0
covers.csv
Normal file
0
covers.csv
Normal file
|
|
2401
dateTaken.csv
Normal file
2401
dateTaken.csv
Normal file
File diff suppressed because it is too large
Load diff
12
dbcvs.sh
Normal file
12
dbcvs.sh
Normal 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
2
dbdown.sh
Normal 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
1
dbnremote.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
sqlite3 -header -csv metadata.db "SELECT COUNT(*) AS nremote FROM entry WHERE origin=1;"
|
||||
2
dbrecord.sh
Normal file
2
dbrecord.sh
Normal 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
7
dbrotate.sh
Normal 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
7
dbrotate2.sh
Normal 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
8
dbrotation.sh
Normal 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
4
dbupload.sh
Normal 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
1
debug.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
fvm flutter run -t lib/main_play.dart --flavor play 2>&1 | tee output.log
|
||||
0
dynamicAlbums.csv
Normal file
0
dynamicAlbums.csv
Normal file
|
|
0
favourites.csv
Normal file
0
favourites.csv
Normal 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();
|
||||
|
|
|
|||
|
|
@ -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 d’ingresso
|
||||
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 (_) {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
791
lib/model/db/db_sqflite.dart.ok
Normal file
791
lib/model/db/db_sqflite.dart.ok
Normal 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 d’ingresso
|
||||
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 (_) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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à l’EXIF → 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 l’EXIF e l’originale 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
718
lib/model/entry/entry.dart.ok
Normal file
718
lib/model/entry/entry.dart.ok
Normal 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à l’EXIF → 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
|
||||
}
|
||||
}
|
||||
653
lib/model/entry/entry.dart.old2
Normal file
653
lib/model/entry/entry.dart.old2
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 dell’immagine 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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
659
lib/model/source/collection_source.dart.ok
Normal file
659
lib/model/source/collection_source.dart.ok
Normal 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 {}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
451
lib/model/source/media_store_source.dart.old
Normal file
451
lib/model/source/media_store_source.dart.old
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
462
lib/model/source/media_store_source.dart.old2
Normal file
462
lib/model/source/media_store_source.dart.old2
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/remote/collection_source_remote_ext.dart
Normal file
25
lib/remote/collection_source_remote_ext.dart
Normal 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)');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 dell’URL 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 l’handle 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 l’elenco 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
@ -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 l’unicità.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 dell’URL 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 l’handle 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 l’elenco 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
27
lib/remote/remote_db_uris.dart
Normal file
27
lib/remote/remote_db_uris.dart
Normal 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://');
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 on‑demand). 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: warm‑up all’avvio 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
// =========================
|
||||
|
|
|
|||
|
|
@ -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 l’unicità.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 dell’URL 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 l’handle 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 l’elenco 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 l’handle 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 l’elenco 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 l’analisi 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 l’analisi 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
31
lib/widgets/viewer/video/remote_video_factory.dart
Normal file
31
lib/widgets/viewer/video/remote_video_factory.dart
Normal 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 l’entry è 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 **remote‑friendly**
|
||||
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 pinch‑out
|
||||
// - 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;
|
||||
|
|
|
|||
562
lib/widgets/viewer/visual/entry_page_view.dart.ok
Normal file
562
lib/widgets/viewer/visual/entry_page_view.dart.ok
Normal 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 **remote‑friendly**
|
||||
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 pinch‑out
|
||||
// - 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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), l’intera 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),
|
||||
),
|
||||
|
|
|
|||
602
lib/widgets/viewer/visual/raster.dart.ok
Normal file
602
lib/widgets/viewer/visual/raster.dart.ok
Normal 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), l’intera 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
3
lines.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
1577
|
||||
1578
|
||||
1579
|
||||
1
ls.db
Normal file
1
ls.db
Normal file
|
|
@ -0,0 +1 @@
|
|||
/data/user/0/deckers.thibault.aves.debug/databases/metadata.db
|
||||
6957
metadata.csv
Normal file
6957
metadata.csv
Normal file
File diff suppressed because it is too large
Load diff
BIN
metadata.db
BIN
metadata.db
Binary file not shown.
Binary file not shown.
BIN
metadata.db.bak
BIN
metadata.db.bak
Binary file not shown.
BIN
metadata_device.db
Normal file
BIN
metadata_device.db
Normal file
Binary file not shown.
16053
output.txt
Normal file
16053
output.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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';
|
||||
|
|
|
|||
217
plugins/aves_video/lib/src/remote_controller.dart
Normal file
217
plugins/aves_video/lib/src/remote_controller.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
130
pubspec.lock
130
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
12
pubspec.yaml
12
pubspec.yaml
|
|
@ -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
101
queued_ids.txt
Normal 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
BIN
rem.zip
Normal file
Binary file not shown.
101
repo_rids_unique.txt
Normal file
101
repo_rids_unique.txt
Normal 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
66
rid_history.txt
Normal 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
0
trash.csv
Normal file
|
|
0
vaults.csv
Normal file
0
vaults.csv
Normal file
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue