Compare commits
893 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d83026ac3 | |||
| d46879260f | |||
| d8c2c5520e | |||
| dc97ae13eb | |||
| 3d22ab93be | |||
| 846c0f18fa | |||
| 4c4d6de245 | |||
| 8dc4005779 | |||
| 3c216e0cb9 | |||
| efe70499ac | |||
| 2003f6843d | |||
| 454edf3ea2 | |||
| e33a270e4d | |||
| 2d26b1d775 | |||
| dcb8cd9ef6 | |||
| af4bc345de | |||
| e0731985be | |||
| 6cf375436b | |||
| df17440b40 | |||
| 0c4dc91e9e | |||
| 98ee328d1b | |||
| 5201126b06 | |||
| 9938beb040 | |||
| 4921a66665 | |||
| 78488cc0d7 | |||
| b95dfa2974 | |||
| e32e696606 | |||
| 5499404267 | |||
| d0f9ad9857 | |||
| b7a94a72fa | |||
| 254d739d12 | |||
| 5224988265 | |||
| 1c69b066f7 | |||
| c6f37f4aa5 | |||
| 9bff630825 | |||
| 10e60b4d94 | |||
| bc7395c2bc | |||
| e2446cd703 | |||
| 1d814aabae | |||
| a65b55a6bf | |||
| a1d39beff4 | |||
| 7c8c6c4303 | |||
| 87afe3191b | |||
| c95a274ff5 | |||
| 324d74f6c3 | |||
| 83e8b670da | |||
| 849add02ad | |||
| 464f343e4a | |||
| 29241b0393 | |||
| ecc708b6cd | |||
| 38c161310a | |||
| 992679e470 | |||
| cccf956fae | |||
| e8100fc958 | |||
| 18a119e9cf | |||
| 6b3813bb9a | |||
| d6ba632c54 | |||
| 479950e60e | |||
| dc60352bd6 | |||
| bf7075629c | |||
| 3c598f459a | |||
| 04f284e27b | |||
| d8ba5b4ddb | |||
| 40c844f128 | |||
| b4c9ff4cae | |||
| 6a82d57957 | |||
| 020ce06ebd | |||
| 4aa9cc3851 | |||
| e0c88fea19 | |||
| 8bd1a708d0 | |||
| aeed6f47a0 | |||
| 9bce035cc9 | |||
| 5d00fe7e78 | |||
| 7cc14bb5d0 | |||
| a6210baf70 | |||
| afb6ca1b5c | |||
| 2b91af9ca5 | |||
| cafec5a37b | |||
| 59713f629a | |||
| b4348691f8 | |||
| a3ec06b4fa | |||
| a574f1ce23 | |||
| f63614bf0f | |||
| f8e128f975 | |||
| 867eed7b16 | |||
| 31cdfc63e6 | |||
| 2dd2054d4c | |||
| 07f32b8df0 | |||
| 7294f0c17e | |||
| 2d3740f3d3 | |||
| d096740cda | |||
| 21b620ee86 | |||
| 08f6a7fbd6 | |||
| f809e438f3 | |||
| 0a228019a9 | |||
| f4a0342007 | |||
| ab863c5bc9 | |||
| 12fcd451bb | |||
| 55ca111b52 | |||
| 56dd6c5920 | |||
| 60f3ee1978 | |||
| fe77aa9ab1 | |||
| dc992ee932 | |||
| b153f22c41 | |||
| c330e14fd8 | |||
| 7459a19e64 | |||
| 0792ef3b1f | |||
| 3de8480630 | |||
| e3fda751ae | |||
| 77d0dc2e1d | |||
| a893ac6e5f | |||
| 756ddebda9 | |||
| 4f861f698c | |||
| 47f09b936c | |||
| 34f98a3cd4 | |||
| d7856fe351 | |||
| 77f5acf2dd | |||
| 87d9512b1f | |||
| 5c9d1afe41 | |||
| 359467d6f5 | |||
| fb77eec2d3 | |||
| 282abf12c9 | |||
| 4a2e8806c1 | |||
| 572eb0cceb | |||
| c5819fe96f | |||
| 396ae8bea3 | |||
| 29cb6c95fd | |||
| 8e0ca68af7 | |||
| 638e569410 | |||
| 23e43a78d9 | |||
| d22591665c | |||
| 621d5cc2bb | |||
| 2e25989a5a | |||
| 5c4a15660a | |||
| b12a8f8af4 | |||
| d4e3b463a3 | |||
| 6cb0342929 | |||
| 0e437eaa6a | |||
| 25091e80c1 | |||
| 317419bde5 | |||
| 5474d29cc7 | |||
| c76fe9bb1d | |||
| d60ff91ae4 | |||
| 3ec0777d63 | |||
| c498d03d6b | |||
| 9e9fa80450 | |||
| 999e944c34 | |||
| 8b8df62ff3 | |||
| b8a53f9239 | |||
| a2df6a7f11 | |||
| 6972edbcb4 | |||
| 13beef4ebe | |||
| a9f037dc8b | |||
| cb00bf66cf | |||
| 9b3a839515 | |||
| b665bd35f6 | |||
| 838eda73e3 | |||
| c5c23e623e | |||
| 2bd9d2844f | |||
| b19c178eae | |||
| a9bb01125e | |||
| ae9fe06f7d | |||
| 9fd8d5aa7c | |||
| 870558b6a9 | |||
| 22a6c8d772 | |||
| 8d813e3d62 | |||
| 75db31914b | |||
| bd22026d16 | |||
| 1ff5075e87 | |||
| 323c0135df | |||
| 6500199d40 | |||
| f2250e7cee | |||
| 0936d4b844 | |||
| 2f32aa6984 | |||
| d7f5ded41a | |||
| 04451ab14e | |||
| 6e8ee38238 | |||
| df0083e2e3 | |||
| c93db99098 | |||
| 7aaec0b0ed | |||
| 6ddd6b84f9 | |||
| f911b09b55 | |||
| 7e7f707c1b | |||
| e9b4385619 | |||
| aaeb5e0308 | |||
| 545f275a44 | |||
| b9fdb774f5 | |||
| 7c30cb0e21 | |||
| 5d5df6f502 | |||
| 08d022d5e2 | |||
| d5d6f9428c | |||
| 27d069bdd5 | |||
| dff5b56a28 | |||
| 79a5a4cb80 | |||
| 25f20de088 | |||
| d1d8e8ed84 | |||
| 1b04ce0ac6 | |||
| 3a4641f32c | |||
| 83ef443e59 | |||
| e23598e361 | |||
| 81c089299e | |||
| d71ca9ae0f | |||
| 3078b7fb89 | |||
| 4e9fe9790b | |||
| 8dad132509 | |||
| 693ae6c7be | |||
| c5b2aa180e | |||
| 54c67bf22c | |||
| f1172c04e4 | |||
| 21346eefe2 | |||
| 42da9abe3e | |||
| 85207b1d2d | |||
| df07276e20 | |||
| a3cea7e6a3 | |||
| 4a87181d3b | |||
| c49c2b28eb | |||
| 586bd3301d | |||
| a3a27dc1c6 | |||
| 7e25eb8587 | |||
| 7bd46dac35 | |||
| 80494a760a | |||
| b2b5854910 | |||
| 8ad6e89ca6 | |||
| f4f898c5c5 | |||
| 4bfa2779ff | |||
| 17928a2f40 | |||
| f2b071ee9d | |||
| fba5b999dd | |||
| bf0902c186 | |||
| 43ef18cdc3 | |||
| 3af5098612 | |||
| 6661983d65 | |||
| 295d80e741 | |||
| 7efde9c74c | |||
| 475dc87604 | |||
| f049e1e2db | |||
| 4b25e3c31e | |||
| ecbd80c55d | |||
| 1fd495f8d8 | |||
| 1e8e02b0ea | |||
| 1a2311e7ba | |||
| 93a4634995 | |||
| 7048d3e106 | |||
| 1faeb083e4 | |||
| 9108b63ba4 | |||
| 3e7eaa58c1 | |||
| af89ebaa42 | |||
| a3f3099cfb | |||
| 5031a6e47b | |||
| 314f3b682b | |||
| c894f654de | |||
| 7e3e21f1a0 | |||
| 19cdcdcab7 | |||
| cd64b9b6eb | |||
| 2b632fd6c5 | |||
| a780ea8dd9 | |||
| bc73e9f1d8 | |||
| 4ea72f5342 | |||
| b227f2a4a5 | |||
| fb4d15d9ef | |||
| 114ef56329 | |||
| 0df5376545 | |||
| 508f025092 | |||
| b41c4cac47 | |||
| 4795c16877 | |||
| a9dc395e19 | |||
| 49afc8c559 | |||
| 1b65243b59 | |||
| 1cf6e030ef | |||
| 0f70c14879 | |||
| e4f493503d | |||
| ab6445d010 | |||
| 188f2bf4f2 | |||
| 9af4751095 | |||
| 6f82ab64a0 | |||
| 5de01f4107 | |||
| c67193d00f | |||
| 683c9a9c69 | |||
| 2e4120d436 | |||
| 892f64829b | |||
| bfe6ed1c12 | |||
| 93f8a42742 | |||
| 4d3e4bbea8 | |||
| c04550fe15 | |||
| be2e95db38 | |||
| 39449b66e1 | |||
| d95833fce7 | |||
| 13f1f37a3e | |||
| d699d3899c | |||
| 4ad33540f3 | |||
| 951418b576 | |||
| 29801d4dd0 | |||
| dd843649cb | |||
| af6b42cec7 | |||
| ec6b6ae779 | |||
| 23fae05694 | |||
| eb49df6ee8 | |||
| 64b1b3ac5d | |||
| 29a24fa047 | |||
| 95bca82355 | |||
| d8407e20da | |||
| e28d72bf9c | |||
| 8d291b1bc3 | |||
| 4d7d451fde | |||
| ef9a4ee643 | |||
| 04c683675f | |||
| f8d14827da | |||
| c560b70b83 | |||
| 62ee9e6560 | |||
| aa70a2e6f3 | |||
| 07d13aed4e | |||
| c0a3406335 | |||
| 0a91597dac | |||
| b90f339476 | |||
| 1f145ef0d4 | |||
| bb409e5ced | |||
| ed5c3f327c | |||
| 72b33a1c52 | |||
| 8c23c07c78 | |||
| 080d845270 | |||
| f2c881cb42 | |||
| 0b3a4e651e | |||
| 9840e8ab57 | |||
| cdb9768335 | |||
| 4e5a8668fc | |||
| 7849c7970e | |||
| 5bf424af40 | |||
| 456fdbe4e9 | |||
| 5802bf7626 | |||
| 62c0f50314 | |||
| cb55a46717 | |||
| 8761451412 | |||
| 819075a8f1 | |||
| 01778c0c42 | |||
| b01e38f4c0 | |||
| c4f06d9830 | |||
| 59d05af9ef | |||
| e63bd150e9 | |||
| bfadfd0fc8 | |||
| 70c866d8f3 | |||
| 28b57f4f42 | |||
| 01e4697a04 | |||
| b116f5e1fd | |||
| 7fb4cd29dc | |||
| 042bf4d4f6 | |||
| 6dac25855b | |||
| 01b0f0bec1 | |||
| f4d1f5ff95 | |||
| e202c108ff | |||
| 848a2ec4f6 | |||
| def5bdca28 | |||
| e0a09cdac7 | |||
| eea2d7b0e0 | |||
| 7e11f3952f | |||
| a9a93e2f8f | |||
| 38f1af7577 | |||
| 155b36a0bc | |||
| 806b35d024 | |||
| 8bbaa60a1b | |||
| bedb3f2bd8 | |||
| 6b0927298a | |||
| d9487ea7e9 | |||
| 8a85d82587 | |||
| 59655ccfd1 | |||
| 6bb8ae0d1e | |||
| 1d80725ea9 | |||
| 353a3ffe00 | |||
| d0e6297995 | |||
| be52ea6f18 | |||
| f76c48c8ae | |||
| 2f203f7a1d | |||
| 14d6e8dd94 | |||
| b10de38e9a | |||
| 22a1544427 | |||
| 4380283ad1 | |||
| 908a2e19fe | |||
| f72afd1359 | |||
| f330ab3731 | |||
| 52f4e60d9b | |||
| 4ddaba06f7 | |||
| bd8a7c038e | |||
| 6f429cdb9c | |||
| e1856e67c2 | |||
| 17fee7fba2 | |||
| 32e0a70024 | |||
| 619c306069 | |||
| 5f92fa0851 | |||
| dbbe118ae9 | |||
| d74e43b6a6 | |||
| 7574b4baef | |||
| 602b33705b | |||
| 53592d1f57 | |||
| ce4ab83ae9 | |||
| f57c5d2f17 | |||
| d9e6e0c956 | |||
| e651bfc04f | |||
| 851a0c015b | |||
| 4bb6bf445b | |||
| e374281d21 | |||
| 7a44fab5e7 | |||
| 29d786d8db | |||
| 441358f1ae | |||
| 7097363f26 | |||
| f2caa59ec9 | |||
| 3337afcf97 | |||
| 53cfe1c609 | |||
| c06ccf2480 | |||
| e38322c4dc | |||
| fa14a1cb6d | |||
| 4c63c01501 | |||
| 74ba072e01 | |||
| fef5578a17 | |||
| b2ee3bf176 | |||
| e47e6de9f4 | |||
| 6c7a3fb809 | |||
| dce264fcae | |||
| b9fea9c045 | |||
| 3d5e5da022 | |||
| 970967104e | |||
| c2543e6238 | |||
| f3d867f9aa | |||
| 6af5f085cf | |||
| 4ac2873a57 | |||
| 240b485a65 | |||
| 31dfac3ece | |||
| 57b9376b83 | |||
| 6618f848ea | |||
| 7d82320c82 | |||
| e7803d305f | |||
| 23928d3c89 | |||
| 7ffe1794d9 | |||
| 3b0523268a | |||
| 5c3326e47d | |||
| f269483072 | |||
| 8717c30498 | |||
| a4dd13f491 | |||
| 823bd024b7 | |||
| eac2301e76 | |||
| 01401b6312 | |||
| 4a93a93839 | |||
| f77ac8cb36 | |||
| d9d94ed321 | |||
| 035a1518ad | |||
| 889a61704f | |||
| b7c0c7a094 | |||
| 3d71e643e1 | |||
| 0492d501f3 | |||
| e38255e895 | |||
| fcf6776fe4 | |||
| 98f285f777 | |||
| 982f4d6a97 | |||
| 972716d884 | |||
| 6f93fdd089 | |||
| f0329cfafa | |||
| 95e14a9ef9 | |||
| c66b57a3b4 | |||
| 71533fb2bc | |||
| cf742c65aa | |||
| 2f5718c92f | |||
| 97ddafe539 | |||
| 0f0937adf5 | |||
| 6eac0f6f98 | |||
| 336727e01a | |||
| 6f36675113 | |||
| 66d4966463 | |||
| e734f7d13d | |||
| f29717272d | |||
| 50a9a907de | |||
| 3cb05b1442 | |||
| b304082ca6 | |||
| f51f9eb371 | |||
| eda451a2e4 | |||
| 4e9ab0cfdc | |||
| 98da48ad73 | |||
| ca1699aabb | |||
| c45019e19c | |||
| 74c394193a | |||
| 0feb9d338f | |||
| 88f9fb2b88 | |||
| 9dc5aedba4 | |||
| cc38579fca | |||
| 52cdb636c9 | |||
| 7e162c99ce | |||
| d294bc0b2a | |||
| e34c61c750 | |||
| 7175fc3444 | |||
| d9c9ed45bb | |||
| 19a00ce582 | |||
| 42d8b97f9b | |||
| af4979fcba | |||
| 7d1dd087fb | |||
| b571ccccaa | |||
| ce39d6ec25 | |||
| 94595a99ac | |||
| adc6398589 | |||
| aa144603e2 | |||
| 7e0082b6f9 | |||
| 5cf65fe676 | |||
| 4624754895 | |||
| 7da6af5138 | |||
| 15cd238223 | |||
| 44b6983b3d | |||
| 09a6e3661f | |||
| efd085cbcf | |||
| 79c3c7f356 | |||
| d060e4c425 | |||
| 28a3dfeeea | |||
| 28ffb9288f | |||
| b530216f21 | |||
| 091049cda9 | |||
| 74437e6809 | |||
| b916907491 | |||
| b045ee1ba6 | |||
| 5d46569137 | |||
| 8875c3f9bb | |||
| 676d8c85a0 | |||
| 8d757ff37a | |||
| 6deab45e80 | |||
| 54bd0c155f | |||
| 6b1e7a8b55 | |||
| dafff19fff | |||
| f2a8ee3c20 | |||
| 3ba3f4bbb2 | |||
| baeec9e2e7 | |||
| 402e494cf7 | |||
| fe4344c518 | |||
| 499b07f988 | |||
| 6b70bdd732 | |||
| 3ab7c18e32 | |||
| 11d6098d2b | |||
| 7e8e1fab11 | |||
| 325ac2b43e | |||
| 9ddadae078 | |||
| c29dc695ae | |||
| 6f85f0a6d5 | |||
| ff965efcae | |||
| c7a43cb36e | |||
| 4554813588 | |||
| cfa6c180e7 | |||
| e36957f00b | |||
| 18deebf728 | |||
| f7e219e9ac | |||
| 5b18769f70 | |||
| 2bc64c2096 | |||
| 9a4e6bef7f | |||
| e8c67999bc | |||
| ae392192f2 | |||
| a9a8e44ef6 | |||
| 1559250f90 | |||
| 3fdd433d89 | |||
| b6143467b6 | |||
| 3cf1ae0aa4 | |||
| 49916667a7 | |||
| b40d25f929 | |||
| 6d0f620629 | |||
| 6d8ab85ef5 | |||
| 51c864fb86 | |||
| 9a5787af29 | |||
| 6ac95ffcd6 | |||
| 41a16b6e83 | |||
| 1b2e0d1b11 | |||
| 517cb437be | |||
| cc384d1f25 | |||
| 1566ed4fda | |||
| 015327829d | |||
| b293b52cf6 | |||
| 4603465783 | |||
| d621ef0b3a | |||
| 56a2fec004 | |||
| f79ac55182 | |||
| add234ce0b | |||
| acbc4c48fa | |||
| 32fa99377d | |||
| 6aa59325d0 | |||
| b3b1d2abf4 | |||
| 14be5c75ee | |||
| 77138aba72 | |||
| 8da397c7ed | |||
| ac75ad7489 | |||
| 5d8c89ba01 | |||
| 2f2352e938 | |||
| 26e9b45687 | |||
| d33b868991 | |||
| 3059008476 | |||
| a79c4babae | |||
| 7b56261719 | |||
| dc56b47e16 | |||
| a773e99214 | |||
| 4fad8f70f3 | |||
| 6857f35910 | |||
| 06976401a5 | |||
| c69bb5b33d | |||
| cde6f127b1 | |||
| 4248a3132a | |||
| 5dac934cc4 | |||
| 47a534c454 | |||
| 655298f44c | |||
| 10c8c3baee | |||
| e284c7a921 | |||
| 17a3185c7a | |||
| 374ecde463 | |||
| 564c8618cb | |||
| eb7a2ab191 | |||
| 98dfec8fdf | |||
| ce43417a26 | |||
| b01e48752d | |||
| d5ff08e9c8 | |||
| e3ee972bf3 | |||
| 1f6e202597 | |||
| f5645a8382 | |||
| 7b038951b0 | |||
| 8b93cf3cec | |||
| bcb15d7ee3 | |||
| 711d0ec092 | |||
| c8abf44666 | |||
| a90f99c445 | |||
| 1d9c3624a9 | |||
| bdbee22f29 | |||
| 82f0bf809d | |||
| 306deb5e6a | |||
| ac6e0acc5d | |||
| 8bcf2f7b82 | |||
| 5598ccf9e6 | |||
| 478d7f989d | |||
| b6f454ba66 | |||
| c613784fc4 | |||
| 6213d3e7a9 | |||
| 6257d261b3 | |||
| 3c24137810 | |||
| 03dbd617f9 | |||
| 8d7aa4b6a2 | |||
| ae99b733e7 | |||
| d5423caf91 | |||
| 2b4adc0c7d | |||
| 52a0aa9d47 | |||
| 22d0a6f19a | |||
| 1f31633b27 | |||
| c673385653 | |||
| e02057a066 | |||
| 4adf840f4b | |||
| 5ce8f549a6 | |||
| 9916d89296 | |||
| 46d33b0cfb | |||
| f56fef667b | |||
| 404c773fd7 | |||
| 9a47e812ed | |||
| 4bd91c0d9e | |||
| c177b4516e | |||
| 2de36cac79 | |||
| e422993c2e | |||
| c89f88de16 | |||
| 0f7e251306 | |||
| 9e264a3b89 | |||
| 673869bd13 | |||
| e40fbbecbb | |||
| 81af5a5654 | |||
| 1699c2ed67 | |||
| 877adee4f7 | |||
| 75f5d46f6a | |||
| 464f2b01a1 | |||
| d22b14734d | |||
| b30491c8fb | |||
| 0c3e8f977c | |||
| 698246ee2a | |||
| 0f4414e359 | |||
| b769043f36 | |||
| 1480829dd1 | |||
| 25629b5a4c | |||
| 641ad196f9 | |||
| 0624b5e7d0 | |||
| 80128b1391 | |||
| ed19ad8819 | |||
| c030a68d12 | |||
| 8f0eeb6c9c | |||
| bd4a0e0a73 | |||
| 02586404d7 | |||
| 86d28ac17f | |||
| 2a39a9105e | |||
| 5e89c1c11c | |||
| 4bad159174 | |||
| fda6458821 | |||
| e4debc89f0 | |||
| c5dec52c02 | |||
| afa267f88b | |||
| e2a81b1f0c | |||
| 699490685f | |||
| 3fb7140587 | |||
| d0b2f6eaec | |||
| dd25cd04c1 | |||
| 1696e90eeb | |||
| 6f73261e21 | |||
| f2bd785c57 | |||
| ec7f70e71c | |||
| c2f9e4637c | |||
| 6e6d69b6b1 | |||
| 6e45ed3c1d | |||
| a17747e2b4 | |||
| e0b62b9cd1 | |||
| 65b6f7df44 | |||
| cf54851342 | |||
| d4993d1b71 | |||
| d8054b3f7b | |||
| 61a6f1ce4e | |||
| 1587923162 | |||
| 10a927f9d7 | |||
| 269af9a2f5 | |||
| 051e360793 | |||
| a46e62eee3 | |||
| 290efebf27 | |||
| 05dbfd94e7 | |||
| d34147803c | |||
| 640b0f84f7 | |||
| 085358d39c | |||
| 8d88467aa0 | |||
| ee51813069 | |||
| 2bdead3e5f | |||
| dee3abd0c3 | |||
| 09ea9deb67 | |||
| 866c4a77a3 | |||
| a856ea20b4 | |||
| e6cb339ff5 | |||
| 7377892942 | |||
| b7321ca204 | |||
| db395f5e37 | |||
| 7891b4de31 | |||
| 15707d933a | |||
| 6516a4285f | |||
| 5a1f09e904 | |||
| a844c636a6 | |||
| 9ade06c6f7 | |||
| b5f4eb79d9 | |||
| cec05194eb | |||
| 22fd34088f | |||
| 04326d17bf | |||
| 4e8142b5b8 | |||
| a29ddaaf1c | |||
| 102811f781 | |||
| b49c64d4ff | |||
| 52b748d935 | |||
| 4d95306cd9 | |||
| 811be77144 | |||
| 6323306693 | |||
| c2b07717fc | |||
| aaad902d6a | |||
| bfb018d1b4 | |||
| 211e0912a3 | |||
| d788585d14 | |||
| 90654dc027 | |||
| 183da5cc84 | |||
| 693b57b14c | |||
| 243f6b3851 | |||
| 988249b177 | |||
| e6a4d1e2b4 | |||
| c07cc7f0df | |||
| 20d8cf6c10 | |||
| efba76380a | |||
| 60f1c6a2b4 | |||
| f9ba8b0072 | |||
| 2173a83196 | |||
| ad04fab21a | |||
| 74c94b9879 | |||
| 3bdd508a81 | |||
| c9438d55e0 | |||
| 33c48412be | |||
| a6cf6ffca8 | |||
| 7d09c4da0d | |||
| b71694fb54 | |||
| 6daa93fd24 | |||
| 3ee68f5127 | |||
| f23b66507f | |||
| 30f478ea80 | |||
| c4374240e8 | |||
| dc6906250d | |||
| e480001585 | |||
| 3df4389fc4 | |||
| aa23de77bc | |||
| ad2bbd0e3c | |||
| 0e636d68c4 | |||
| e59789f777 | |||
| a0549b304f | |||
| 69d762c131 | |||
| e9e3340c08 | |||
| 0d8af29eeb | |||
| 60f9391b4e | |||
| 56f4b1f224 | |||
| c123df4637 | |||
| 1cf48b3ab7 | |||
| ee73c0e282 | |||
| b7b1175207 | |||
| 9903a6bde8 | |||
| da0b4bb94a | |||
| 99042352fb | |||
| a132435967 | |||
| 417c345946 | |||
| c75d0a62b5 | |||
| 6f6e6ad326 | |||
| 32e7750b7d | |||
| d28331c9d2 | |||
| 117aaf66a8 | |||
| 274d90aa11 | |||
| 3544fadc74 | |||
| 2975acd8db | |||
| fe51ad5d4b | |||
| a59168f8b6 | |||
| 6b5ee52dc7 | |||
| f61bbfa77a | |||
| 6283754ce5 | |||
| ed78bca33e | |||
| 9e38b5231c | |||
| 4706493057 | |||
| 18a76c6bc2 | |||
| 104e200b4a | |||
| 9af351e0dd | |||
| 1ddfe956a7 | |||
| 40efaeafe1 | |||
| 2b4cdfaee4 | |||
| e2324df0cc | |||
| 447ddfd8d5 | |||
| 96a932264b | |||
| d04161cc9e | |||
| 03c45f676a | |||
| 56fa39387f | |||
| 60704ad3a7 | |||
| e3fd17f550 | |||
| 1dcf49200a | |||
| d5638c6204 | |||
| bf97dce150 | |||
| 9e80f47e9f | |||
| 963b85f756 | |||
| fc141cb92a | |||
| 0bdddb365f | |||
| 0521c362c9 | |||
| 8cc51c7f71 | |||
| eafce1ee5d | |||
| 341543da24 | |||
| 65afc99ead | |||
| c301def4bc | |||
| a33a7c0844 | |||
| 710196d514 | |||
| 216065fb38 | |||
| 708c4b6905 | |||
| 3536c359f3 | |||
| a3dbf6ff24 | |||
| 13235f3a37 | |||
| 74636f4b1a | |||
| 9275d9ab55 | |||
| 89249c9895 | |||
| 9b85b009a4 | |||
| e017617612 | |||
| 38f2a93c0f | |||
| 143dfc6ad2 | |||
| d82967ea3d | |||
| eab7571feb | |||
| 09eab48b1b | |||
| 9196a24eb1 | |||
| 4f9eb19ead | |||
| 3d33d50c63 | |||
| 9e17ecbd4a | |||
| a535d22f78 | |||
| 826602afb6 | |||
| b35aa30ea7 | |||
| 330b7990fe | |||
| 84330af417 | |||
| e0945e4409 | |||
| ac8e53b9a7 | |||
| bdc9e43e9e | |||
| 88102b9705 | |||
| 6d7e8cdcb9 | |||
| 359ccdd052 | |||
| 57ca396b46 | |||
| 78d2881b4b | |||
| cafce0ed5f | |||
| 3adde57db9 | |||
| 503ecb8076 | |||
| 01d6dbba86 | |||
| b0bcad861f | |||
| d70a8848ec | |||
| c8426e71eb | |||
| b523e47102 | |||
| 57dde4c922 | |||
| 34c16015e5 | |||
| 8387492a9b | |||
| 114ba42af7 | |||
| aeaf52cb58 | |||
| 1e55a3f21a | |||
| 4fdb19d002 | |||
| d30c9f7120 | |||
| 9697bffc5e | |||
| 65249a4ee7 | |||
| d5b72c3d3a | |||
| 86bd55268f | |||
| 8563342d0f | |||
| d057b61158 |
@@ -3,7 +3,7 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v1.7.0)
|
- To the latest version of the app (stable is v1.8.0)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🐞 Bug report"
|
|
||||||
about: Report a bug
|
|
||||||
title: "[Bug] <Write short description here>"
|
|
||||||
labels: "bug"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v1.7.0)
|
|
||||||
- All extensions
|
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Device information
|
|
||||||
* Tachiyomi version: ?
|
|
||||||
* Android version: ?
|
|
||||||
* Device: ?
|
|
||||||
|
|
||||||
## Steps to reproduce
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
This should happen.
|
|
||||||
|
|
||||||
### Actual behavior
|
|
||||||
This happened instead.
|
|
||||||
|
|
||||||
## Other details
|
|
||||||
Additional details and attachments.
|
|
||||||
|
|
||||||
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Tachiyomi help website
|
- name: ⚠️ Extension/source issue
|
||||||
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
|
- name: 📦 Tachiyomi extensions
|
||||||
|
url: https://tachiyomi.org/extensions
|
||||||
|
about: List of all available extensions with download links
|
||||||
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Common questions are answered here.
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
- name: Tachiyomi extensions GitHub repository
|
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🌟 Feature request"
|
|
||||||
about: Suggest a feature to improve Tachiyomi
|
|
||||||
title: "[Feature Request] <Write short description here>"
|
|
||||||
labels: "feature"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated:
|
|
||||||
- To the latest version of the app (stable is v1.7.0)
|
|
||||||
- All extensions
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
|
||||||
- I will fill out the title and the information in this template
|
|
||||||
|
|
||||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why/User Benefit/User Problem
|
|
||||||
(explain why this feature should be added)
|
|
||||||
|
|
||||||
## What/Requirements
|
|
||||||
(explain how this feature would behave)
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
name: 🐞 Issue report
|
||||||
|
description: Report an issue in Tachiyomi
|
||||||
|
labels: [Bug]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide an example of the issue.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. Issue here
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Explain what you should expect to happen.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This should happen..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: Explain what actually happens.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This happened instead..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: crash-logs
|
||||||
|
attributes:
|
||||||
|
label: Crash logs
|
||||||
|
description: |
|
||||||
|
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||||
|
placeholder: |
|
||||||
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tachiyomi-version
|
||||||
|
attributes:
|
||||||
|
label: Tachiyomi version
|
||||||
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "1.8.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Android 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Google Pixel 5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Suggest a feature to improve Tachiyomi
|
||||||
|
labels: [Feature request]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Describe your suggested feature
|
||||||
|
description: How can Tachiyomi be improved?
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"It should work like this..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Extension/source/catalogue issue"
|
|
||||||
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
labels: "catalog, invalid"
|
|
||||||
---
|
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
Please include a summary of the change and which issue is fixed.
|
||||||
|
Also make sure you've tested your code and also done a self-review of it.
|
||||||
|
Don't forget to check all base themes and tablet mode for relevant changes.
|
||||||
|
|
||||||
|
If your changes are visual, please provide images below:
|
||||||
|
|
||||||
|
### Images
|
||||||
|
| Image 1 | Image 2 |
|
||||||
|
| ------- | ------- |
|
||||||
|
|  |  |
|
||||||
|
-->
|
||||||
@@ -20,7 +20,6 @@ jobs:
|
|||||||
preview:
|
preview:
|
||||||
name: Build app preview
|
name: Build app preview
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -32,10 +31,10 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 1.8
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 11
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
@@ -53,12 +52,9 @@ jobs:
|
|||||||
write-mode: overwrite # optional, default is preserve
|
write-mode: overwrite # optional, default is preserve
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleRelease --stacktrace
|
arguments: assembleStandardRelease --stacktrace
|
||||||
wrapper-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
uses: r0adkll/sign-android-release@v1
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
name: Build app
|
name: Build app
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -28,10 +27,10 @@ jobs:
|
|||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JDK 1.8
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 1.8
|
java-version: 11
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
- name: Copy CI gradle.properties
|
||||||
run: |
|
run: |
|
||||||
@@ -39,12 +38,10 @@ jobs:
|
|||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Build app
|
- name: Build app
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: assembleStandardDebug
|
arguments: assembleDevDebug
|
||||||
wrapper-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -13,16 +13,6 @@ jobs:
|
|||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
rules: |
|
rules: |
|
||||||
[
|
[
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
|
|
||||||
"message": "It was not opened in the correct repo, as the template mentioned."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "title",
|
|
||||||
"regex": ".*<Write short description here>*",
|
|
||||||
"message": "The description in the title was not filled out."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
@@ -32,5 +22,11 @@ jobs:
|
|||||||
"type": "body",
|
"type": "body",
|
||||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
"message": "Requested information in the template was not filled out."
|
"message": "Requested information in the template was not filled out."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1.0
|
uses: tachiyomiorg/issue-moderator-action@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: Lock threads
|
|||||||
on:
|
on:
|
||||||
# Daily
|
# Daily
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 * * * *'
|
- cron: '0 0 * * *'
|
||||||
# Manual trigger
|
# Manual trigger
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -12,8 +12,8 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: '2'
|
issue-inactive-days: '2'
|
||||||
pr-lock-inactive-days: '2'
|
pr-inactive-days: '2'
|
||||||
|
|||||||
+99
-49
@@ -1,76 +1,126 @@
|
|||||||
# Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
contributors and maintainers pledge to making participation in our project and
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
level of experience, education, socio-economic status, nationality, personal
|
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||||
appearance, race, religion, or sexual identity and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to a positive environment for our
|
||||||
include:
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
advances
|
advances of any kind
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
* Publishing others' private information, such as a physical or email
|
||||||
address, without explicit permission
|
address, without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
Community moderators are responsible for clarifying and enforcing our standards of
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
response to any instances of unacceptable behavior.
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
Community moderators have the right and responsibility to remove, edit, or reject
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
decisions when appropriate.
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
when an individual is representing the project or its community. Examples of
|
an individual is officially representing the community in public spaces.
|
||||||
representing a project or community include using an official project e-mail
|
Examples of representing our community include using an official e-mail address,
|
||||||
address, posting via an official social media account, or acting as an appointed
|
posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event. Representation of a project may be
|
representative at an online or offline event.
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
reported to the community moderators responsible for enforcement at
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
reporter of any incident.
|
||||||
members of the project's leadership.
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community moderators will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community moderators, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
version 2.1, available at
|
||||||
|
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq
|
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||||
|
at [translations](https://www.contributor-covenant.org/translations).
|
||||||
|
|||||||
+2
-1
@@ -10,6 +10,7 @@ Thanks for your interest in contributing to Tachiyomi!
|
|||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||||
|
You do not need to ask for permission nor an assignment.
|
||||||
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
@@ -26,7 +27,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ Tachiyomi is a free and open source manga reader for Android 6.0 and above. This
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features of Tachiyomi(original) include:
|
Features of Tachiyomi(original) include:
|
||||||
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded content
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@@ -84,13 +84,12 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (More > About > Version)
|
* Include version (More → About → Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
|
|||||||
+103
-110
@@ -11,9 +11,6 @@ plugins {
|
|||||||
kotlin("plugin.parcelize")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
// Realm (EH)
|
|
||||||
kotlin("kapt")
|
|
||||||
id("realm-android")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
||||||
@@ -25,32 +22,25 @@ if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
|
|||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion(AndroidConfig.compileSdk)
|
compileSdk = AndroidConfig.compileSdk
|
||||||
buildToolsVersion(AndroidConfig.buildTools)
|
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi.sy"
|
applicationId = "eu.kanade.tachiyomi.sy"
|
||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
versionCode = 25
|
||||||
versionCode = 19
|
versionName = "1.8.0"
|
||||||
versionName = "1.7.0"
|
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
}
|
}
|
||||||
}
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -71,7 +61,7 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions("default")
|
flavorDimensions += "default"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("standard") {
|
create("standard") {
|
||||||
@@ -82,31 +72,41 @@ android {
|
|||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("dev") {
|
create("dev") {
|
||||||
resConfigs("en", "xxhdpi")
|
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude("META-INF/DEPENDENCIES")
|
resources.excludes.addAll(listOf(
|
||||||
exclude("LICENSE.txt")
|
"META-INF/DEPENDENCIES",
|
||||||
exclude("META-INF/LICENSE")
|
"LICENSE.txt",
|
||||||
exclude("META-INF/LICENSE.txt")
|
"META-INF/LICENSE",
|
||||||
exclude("META-INF/NOTICE")
|
"META-INF/LICENSE.txt",
|
||||||
exclude("META-INF/*.kotlin_module")
|
"META-INF/README.md",
|
||||||
|
"META-INF/NOTICE",
|
||||||
// Compatibility for two RxJava versions (EXH)
|
"META-INF/*.kotlin_module",
|
||||||
exclude("META-INF/rxjava.properties")
|
"META-INF/*.version",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
buildFeatures {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
viewBinding = true
|
||||||
isAbortOnError = false
|
|
||||||
isCheckReleaseBuilds = false
|
// Disable some unused things
|
||||||
|
aidl = false
|
||||||
|
renderScript = false
|
||||||
|
shaders = false
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||||
|
abortOnError = false
|
||||||
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -120,78 +120,78 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.6.0"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("org.tachiyomi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
implementation("androidx.annotation:annotation:1.4.0-alpha01")
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha01")
|
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.browser:browser:1.4.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.core:core-ktx:1.8.0-alpha02")
|
||||||
implementation("androidx.core:core-ktx:1.6.0-beta01")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.4.0-alpha01"
|
val lifecycleVersion = "2.4.0"
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.7.0-alpha03")
|
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||||
|
|
||||||
// UI library
|
// RX
|
||||||
implementation("com.google.android.material:material:1.4.0-beta01")
|
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
|
||||||
|
|
||||||
// ReactiveX
|
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.9.1"
|
val okhttpVersion = "4.9.1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:3.0.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||||
|
|
||||||
// JSON
|
// Data serialization (JSON, protobuf)
|
||||||
val kotlinSerializationVersion = "1.1.0"
|
val kotlinSerializationVersion = "1.3.2"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
implementation("app.cash.quickjs:quickjs-android:0.9.2")
|
||||||
|
// TODO: remove Duktape once all extensions are using QuickJS
|
||||||
|
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation("org.jsoup:jsoup:1.14.3")
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("com.github.requery:sqlite-android:3.35.5")
|
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
@@ -202,57 +202,59 @@ dependencies {
|
|||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image loading
|
||||||
val coilVersion = "1.2.0"
|
val coilVersion = "1.4.0"
|
||||||
implementation("io.coil-kt:coil:$coilVersion")
|
implementation("io.coil-kt:coil:$coilVersion")
|
||||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation("com.github.tachiyomiorg:image-decoder:7a44c9b")
|
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||||
|
|
||||||
// Logging
|
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
|
||||||
|
|
||||||
// Crash reports
|
|
||||||
//implementation("ch.acra:acra-http:5.7.0")
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// UI
|
// UI libraries
|
||||||
|
implementation("com.google.android.material:material:1.6.0-alpha02")
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
|
}
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
|
||||||
val materialDialogsVersion = "3.1.1"
|
|
||||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
val conductorVersion = "3.1.2"
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||||
exclude(group = "com.android.support")
|
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||||
}
|
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "1.0.0"
|
val flowbindingVersion = "1.2.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation("com.squareup.logcat:logcat:0.1")
|
||||||
|
|
||||||
|
// Crash reports/analytics
|
||||||
|
//implementation("ch.acra:acra-http:5.8.4")
|
||||||
|
//"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
|
// Shizuku
|
||||||
|
val shizukuVersion = "12.1.0"
|
||||||
|
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||||
|
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
@@ -261,22 +263,12 @@ dependencies {
|
|||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
val robolectricVersion = "3.1.4"
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.5.0"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
// [EXH] Android 7 SSL Workaround
|
|
||||||
implementation("com.google.android.gms:play-services-safetynet:17.0.0")
|
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0")
|
||||||
|
|
||||||
@@ -284,11 +276,11 @@ dependencies {
|
|||||||
implementation ("info.debatty:java-string-similarity:2.0.0")
|
implementation ("info.debatty:java-string-similarity:2.0.0")
|
||||||
|
|
||||||
// Firebase (EH)
|
// Firebase (EH)
|
||||||
implementation("com.google.firebase:firebase-analytics-ktx:19.0.0")
|
implementation("com.google.firebase:firebase-analytics-ktx:20.0.2")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-ktx:18.0.0")
|
implementation("com.google.firebase:firebase-crashlytics-ktx:18.2.7")
|
||||||
|
|
||||||
// Better logging (EH)
|
// Better logging (EH)
|
||||||
implementation("com.elvishew:xlog:1.9.0")
|
implementation("com.elvishew:xlog:1.11.0")
|
||||||
|
|
||||||
// Debug utils (EH)
|
// Debug utils (EH)
|
||||||
val debugOverlayVersion = "1.1.3"
|
val debugOverlayVersion = "1.1.3"
|
||||||
@@ -307,12 +299,13 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-Xopt-in=kotlin.Experimental",
|
"-Xopt-in=kotlin.Experimental",
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
"-Xopt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
"-Xopt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-Xopt-in=kotlin.time.ExperimentalTime",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
-30
@@ -69,16 +69,19 @@
|
|||||||
|
|
||||||
# Keep extension's common dependencies
|
# Keep extension's common dependencies
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
-keep,allowoptimization class androidx.preference.** { *; }
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlinx.serialization.** { *; }
|
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||||
|
|
||||||
# RxJava 1.1.0
|
# RxJava 1.1.0
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
@@ -104,33 +107,6 @@
|
|||||||
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
|
# === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard
|
||||||
-dontwarn okio.**
|
-dontwarn okio.**
|
||||||
|
|
||||||
# === GSON: https://raw.githubusercontent.com/google/gson/master/examples/android-proguard-example/proguard.cfg
|
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
|
||||||
# removes such information by default, so configure it to keep all of it.
|
|
||||||
-keepattributes Signature
|
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# Gson specific classes
|
|
||||||
-dontwarn sun.misc.**
|
|
||||||
#-keep class com.google.gson.stream.** { *; }
|
|
||||||
|
|
||||||
# Application classes that will be serialized/deserialized over Gson
|
|
||||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory, TypeAdapter,
|
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
|
||||||
-keep class * extends com.google.gson.TypeAdapter
|
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
|
||||||
|
|
||||||
# Prevent R8 from leaving Data object members always null
|
|
||||||
-keepclassmembers,allowobfuscation class * {
|
|
||||||
@com.google.gson.annotations.SerializedName <fields>;
|
|
||||||
}
|
|
||||||
|
|
||||||
# == Nucleus
|
# == Nucleus
|
||||||
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
-keepclassmembers class * extends nucleus.presenter.Presenter {
|
||||||
<init>();
|
<init>();
|
||||||
|
|||||||
+187
-186
@@ -19,13 +19,13 @@
|
|||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@@ -33,12 +33,15 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Base"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Splash">
|
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@@ -52,7 +55,8 @@
|
|||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search">
|
android:label="@string/action_global_search"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@@ -73,9 +77,11 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -83,15 +89,26 @@
|
|||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.UnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Base" />
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:configChanges="uiMode|orientation|screenSize"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -105,7 +122,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList">
|
android:label="MyAnimeList"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -119,7 +137,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -133,7 +152,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi">
|
android:label="Bangumi"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@@ -146,23 +166,10 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.login.EhLoginActivity"
|
android:name="exh.ui.login.EhLoginActivity"
|
||||||
android:label="EHentaiLogin" />
|
android:label="EHentaiLogin"
|
||||||
|
android:exported="false"/>
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
@@ -177,7 +184,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.UpdaterService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
@@ -188,189 +195,183 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
<!-- EH -->
|
<!-- EH -->
|
||||||
<service
|
|
||||||
android:name="exh.eh.EHentaiUpdateWorker"
|
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
|
||||||
android:exported="true" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.intercept.InterceptActivity"
|
android:name="exh.ui.intercept.InterceptActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.Base">
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="true">
|
||||||
|
<!-- E-Hentai -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<!-- EH -->
|
<data android:scheme="https" />
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="g.e-hentai.org"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="g.e-hentai.org"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="e-hentai.org"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="e-hentai.org"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- EXH -->
|
<data android:host="e-hentai.org" />
|
||||||
<data
|
<data android:host="www.e-hentai.org" />
|
||||||
android:host="exhentai.org"
|
<data android:host="g.e-hentai.org" />
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="exhentai.org"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- nhentai -->
|
<data android:pathPattern="/g/..*" />
|
||||||
<data
|
</intent-filter>
|
||||||
android:host="nhentai.net"
|
<!-- ExHentai -->
|
||||||
android:pathPrefix="/g/"
|
<intent-filter>
|
||||||
android:scheme="http" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data
|
|
||||||
android:host="nhentai.net"
|
|
||||||
android:pathPrefix="/g/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- Perv Eden -->
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
android:host="www.perveden.com"
|
|
||||||
android:pathPattern="/.*/.*-manga/.*"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="www.perveden.com"
|
|
||||||
android:pathPattern="/.*/.*-manga/.*"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- Hentai Cafe -->
|
<data android:scheme="https" />
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="hentai.cafe"
|
|
||||||
android:pathPrefix="/hc.fyi/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="hentai.cafe"
|
|
||||||
android:pathPrefix="/hc.fyi/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- Tsumino -->
|
<data android:host="exhentai.org" />
|
||||||
<data
|
<data android:host="www.exhentai.org" />
|
||||||
android:host="www.tsumino.com"
|
|
||||||
android:pathPrefix="/Book/Info/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="www.tsumino.com"
|
|
||||||
android:pathPrefix="/Book/Info/"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="www.tsumino.com"
|
|
||||||
android:pathPrefix="/Read/View/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="www.tsumino.com"
|
|
||||||
android:pathPrefix="/Read/View/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- Hitomi.la -->
|
<data android:pathPattern="/g/..*" />
|
||||||
<data
|
</intent-filter>
|
||||||
android:host="hitomi.la"
|
<!-- NHentai -->
|
||||||
android:pathPrefix="/galleries/"
|
<intent-filter>
|
||||||
android:scheme="http" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPrefix="/reader/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPrefix="/galleries/"
|
|
||||||
android:scheme="https" />
|
|
||||||
<data
|
|
||||||
android:host="hitomi.la"
|
|
||||||
android:pathPrefix="/reader/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- Pururin.io -->
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
android:host="pururin.io"
|
|
||||||
android:pathPrefix="/gallery/"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="pururin.io"
|
|
||||||
android:pathPrefix="/gallery/"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- HBrowse -->
|
<data android:scheme="https" />
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:host="www.hbrowse.com"
|
|
||||||
android:scheme="http" />
|
|
||||||
<data
|
|
||||||
android:host="www.hbrowse.com"
|
|
||||||
android:scheme="https" />
|
|
||||||
|
|
||||||
<!-- MangaDex -->
|
<data android:host="nhentai.net" />
|
||||||
<!--<data
|
<data android:host="www.nhentai.net" />
|
||||||
android:scheme="https"
|
|
||||||
android:host="www.mangadex.org"
|
|
||||||
android:pathPrefix="/manga/" />
|
|
||||||
<data
|
|
||||||
android:scheme="https"
|
|
||||||
android:host="mangadex.org"
|
|
||||||
android:pathPrefix="/manga/" />
|
|
||||||
<data
|
|
||||||
android:scheme="https"
|
|
||||||
android:host="www.mangadex.cc"
|
|
||||||
android:pathPrefix="/manga/" />
|
|
||||||
<data
|
|
||||||
android:scheme="https"
|
|
||||||
android:host="www.mangadex.cc"
|
|
||||||
android:pathPrefix="/manga/" />
|
|
||||||
|
|
||||||
<data
|
<data android:pathPattern="/g/..*" />
|
||||||
android:scheme="https"
|
</intent-filter>
|
||||||
android:host="www.mangadex.org"
|
<!-- Perv Eden -->
|
||||||
android:pathPrefix="/title/" />
|
<intent-filter>
|
||||||
<data
|
<action android:name="android.intent.action.VIEW" />
|
||||||
android:scheme="https"
|
|
||||||
android:host="mangadex.org"
|
|
||||||
android:pathPrefix="/title/" />
|
|
||||||
<data
|
|
||||||
android:scheme="https"
|
|
||||||
android:host="www.mangadex.cc"
|
|
||||||
android:pathPrefix="/title/" />
|
|
||||||
<data
|
|
||||||
android:scheme="https"
|
|
||||||
android:host="www.mangadex.cc"
|
|
||||||
android:pathPrefix="/title/" />
|
|
||||||
|
|
||||||
<data
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
android:scheme="https"
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
android:host="www.mangadex.org"
|
|
||||||
android:pathPrefix="/chapter/" />
|
<data android:scheme="https" />
|
||||||
<data
|
<data android:scheme="http" />
|
||||||
android:scheme="https"
|
|
||||||
android:host="mangadex.org"
|
<data android:host="perveden.com" />
|
||||||
android:pathPrefix="/chapter/" />
|
<data android:host="www.perveden.com" />
|
||||||
<data
|
|
||||||
android:scheme="https"
|
<data android:pathPattern="/.*/.*-manga/.*" />
|
||||||
android:host="www.mangadex.cc"
|
</intent-filter>
|
||||||
android:pathPrefix="/chapter/" />
|
<!-- Tsumino -->
|
||||||
<data
|
<intent-filter>
|
||||||
android:scheme="https"
|
<action android:name="android.intent.action.VIEW" />
|
||||||
android:host="www.mangadex.cc"
|
|
||||||
android:pathPrefix="/chapter/" />-->
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
|
||||||
|
<data android:host="tsumino.com" />
|
||||||
|
<data android:host="www.tsumino.com" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/Read/View/..*" />
|
||||||
|
<data android:pathPattern="/Book/Info/..*" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Hitomi.la -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
|
||||||
|
<data android:host="hitomi.la" />
|
||||||
|
<data android:host="www.hitomi.la" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/reader/..*" />
|
||||||
|
<data android:pathPattern="/galleries/..*" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Pururin -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
|
||||||
|
<data android:host="pururin.io" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/gallery/..*" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- HBrowse -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
|
||||||
|
<data android:host="hbrowse.com" />
|
||||||
|
<data android:host="www.hbrowse.com" />
|
||||||
|
|
||||||
|
<!--<data android:pathPattern="/gallery/..*" />-->
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Mangadex -->
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<data android:host="mangadex.org" />
|
||||||
|
<data android:host="mangadex.cc" />
|
||||||
|
<data android:host="www.mangadex.org" />
|
||||||
|
<data android:host="www.mangadex.cc" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/manga/..*" />
|
||||||
|
<data android:pathPattern="/title/..*" />
|
||||||
|
<data android:pathPattern="/chapter/..*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="exh.ui.captcha.BrowserActionActivity"
|
android:name="exh.ui.captcha.BrowserActionActivity"
|
||||||
android:theme="@style/Theme.Base" />
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="false"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -7,23 +7,22 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.multidex.MultiDex
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import coil.decode.GifDecoder
|
import coil.decode.GifDecoder
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import coil.util.DebugLogger
|
||||||
import com.elvishew.xlog.LogConfiguration
|
import com.elvishew.xlog.LogConfiguration
|
||||||
import com.elvishew.xlog.LogLevel
|
import com.elvishew.xlog.LogLevel
|
||||||
import com.elvishew.xlog.XLog
|
import com.elvishew.xlog.XLog
|
||||||
@@ -32,62 +31,59 @@ import com.elvishew.xlog.printer.Printer
|
|||||||
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
|
||||||
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy
|
||||||
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
|
||||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
|
||||||
import com.google.firebase.analytics.ktx.analytics
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
import com.ms_square.debugoverlay.DebugOverlay
|
import com.ms_square.debugoverlay.DebugOverlay
|
||||||
import com.ms_square.debugoverlay.modules.FpsModule
|
import com.ms_square.debugoverlay.modules.FpsModule
|
||||||
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||||
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import exh.debug.DebugToggles
|
import exh.debug.DebugToggles
|
||||||
import exh.log.CrashlyticsPrinter
|
import exh.log.CrashlyticsPrinter
|
||||||
import exh.log.EHDebugModeOverlay
|
import exh.log.EHDebugModeOverlay
|
||||||
import exh.log.EHLogLevel
|
import exh.log.EHLogLevel
|
||||||
import exh.log.EnhancedFilePrinter
|
import exh.log.EnhancedFilePrinter
|
||||||
import exh.log.XLogTree
|
import exh.log.XLogLogcatLogger
|
||||||
import exh.log.xLogD
|
import exh.log.xLogD
|
||||||
import exh.log.xLogE
|
import exh.log.xLogE
|
||||||
import exh.syDebugVersion
|
import exh.syDebugVersion
|
||||||
import io.realm.Realm
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import logcat.LogPriority
|
||||||
|
import logcat.LogcatLogger
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.NoSuchAlgorithmException
|
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.net.ssl.SSLContext
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
import kotlin.time.days
|
|
||||||
|
|
||||||
open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super<Application>.onCreate()
|
||||||
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
setupExhLogging() // EXH logging
|
setupExhLogging() // EXH logging
|
||||||
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
|
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
|
||||||
if (!BuildConfig.DEBUG) addAnalytics()
|
if (!BuildConfig.DEBUG) addAnalytics()
|
||||||
|
|
||||||
workaroundAndroid7BrokenSSL()
|
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
@@ -102,13 +98,10 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
Realm.init(this)
|
|
||||||
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
|
||||||
setupDebugOverlay()
|
setupDebugOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
// Show notification to disable Incognito Mode when it's enabled
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
@@ -120,7 +113,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||||
setContentTitle(getString(R.string.pref_incognito_mode))
|
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||||
setContentText(getString(R.string.notification_incognito_text))
|
setContentText(getString(R.string.notification_incognito_text))
|
||||||
setSmallIcon(R.drawable.ic_glasses_black_24dp)
|
setSmallIcon(R.drawable.ic_glasses_24dp)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getBroadcast(
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
@@ -138,16 +131,21 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
preferences.themeMode()
|
||||||
super.attachBaseContext(base)
|
.asImmediateFlow {
|
||||||
MultiDex.install(this)
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
}
|
when (it) {
|
||||||
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
/*if (!LogcatLogger.isInstalled && preferences.verboseLogging()) {
|
||||||
super.onConfigurationChanged(newConfig)
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
@@ -158,51 +156,35 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
} else {
|
} else {
|
||||||
add(GifDecoder())
|
add(GifDecoder())
|
||||||
}
|
}
|
||||||
|
add(TachiyomiImageDecoder(this@App.resources))
|
||||||
add(ByteBufferFetcher())
|
add(ByteBufferFetcher())
|
||||||
add(MangaCoverFetcher())
|
add(MangaCoverFetcher())
|
||||||
}
|
}
|
||||||
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||||
crossfade(300)
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
if (preferences.verboseLogging()) logger(DebugLogger())
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun workaroundAndroid7BrokenSSL() {
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N ||
|
|
||||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
SSLContext.getInstance("TLSv1.2")
|
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
|
||||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ProviderInstaller.installIfNeeded(applicationContext)
|
|
||||||
} catch (e: GooglePlayServicesRepairableException) {
|
|
||||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
|
||||||
} catch (e: GooglePlayServicesNotAvailableException) {
|
|
||||||
xLogE("Could not install Android 7 broken SSL workaround!", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addAnalytics() {
|
private fun addAnalytics() {
|
||||||
if (syDebugVersion != "0") {
|
if (syDebugVersion != "0") {
|
||||||
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
@Suppress("unused")
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
fun onAppBackgrounded() {
|
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
try {
|
||||||
|
Notifications.createChannels(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXH
|
// EXH
|
||||||
@@ -231,7 +213,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
printers += EnhancedFilePrinter
|
printers += EnhancedFilePrinter
|
||||||
.Builder(logFolder.absolutePath) {
|
.Builder(logFolder.absolutePath) {
|
||||||
fileNameGenerator = object : DateFileNameGenerator() {
|
fileNameGenerator = object : DateFileNameGenerator() {
|
||||||
@@ -245,7 +226,7 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
flattener { timeMillis, level, tag, message ->
|
flattener { timeMillis, level, tag, message ->
|
||||||
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
|
||||||
}
|
}
|
||||||
cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
|
cleanStrategy = FileLastModifiedCleanStrategy(7.days.inWholeMilliseconds)
|
||||||
backupStrategy = NeverBackupStrategy()
|
backupStrategy = NeverBackupStrategy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,15 +242,17 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
|
|
||||||
xLogD("Application booting...")
|
xLogD("Application booting...")
|
||||||
xLogD(
|
xLogD(
|
||||||
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
|
"""
|
||||||
"Preview build: $syDebugVersion\n" +
|
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
|
||||||
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
|
Preview build: $syDebugVersion
|
||||||
"Android build ID: ${Build.DISPLAY}\n" +
|
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
|
||||||
"Device brand: ${Build.BRAND}\n" +
|
Android build ID: ${Build.DISPLAY}
|
||||||
"Device manufacturer: ${Build.MANUFACTURER}\n" +
|
Device brand: ${Build.BRAND}
|
||||||
"Device name: ${Build.DEVICE}\n" +
|
Device manufacturer: ${Build.MANUFACTURER}
|
||||||
"Device model: ${Build.MODEL}\n" +
|
Device name: ${Build.DEVICE}
|
||||||
"Device product name: ${Build.PRODUCT}"
|
Device model: ${Build.MODEL}
|
||||||
|
Device product name: ${Build.PRODUCT}
|
||||||
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,8 +293,6 @@ open class App : Application(), LifecycleObserver, ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by extensions.
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.3
|
||||||
|
*/
|
||||||
|
object AppInfo {
|
||||||
|
fun getVersionCode() = BuildConfig.VERSION_CODE
|
||||||
|
fun getVersionName() = BuildConfig.VERSION_NAME
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
|||||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@@ -25,6 +26,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
@@ -43,7 +46,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
addSingletonFactory { CustomMangaManager(app) }
|
addSingletonFactory { CustomMangaManager(app) }
|
||||||
@@ -52,7 +55,7 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
Handler().post {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.util.preference.minusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.preference.plusAssign
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@@ -32,30 +37,29 @@ object Migrations {
|
|||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
|
|
||||||
// Cancel app updater job for debug builds that don't include it
|
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.cancelTask(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
// Always set up background tasks to ensure they're running
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
AppUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
|
||||||
// Fresh install
|
// Fresh install
|
||||||
if (oldVersion == 0) {
|
if (oldVersion == 0) {
|
||||||
// Set up default background tasks
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.setupTask(context)
|
|
||||||
}
|
|
||||||
ExtensionUpdateJob.setupTask(context)
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
if (oldVersion < 14) {
|
if (oldVersion < 14) {
|
||||||
// Restore jobs after upgrading to Evernote's job scheduler.
|
// Restore jobs after upgrading to Evernote's job scheduler.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
@@ -88,7 +92,7 @@ object Migrations {
|
|||||||
if (oldVersion < 43) {
|
if (oldVersion < 43) {
|
||||||
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
UpdaterJob.setupTask(context)
|
AppUpdateJob.setupTask(context)
|
||||||
}
|
}
|
||||||
LibraryUpdateJob.setupTask(context)
|
LibraryUpdateJob.setupTask(context)
|
||||||
BackupCreatorJob.setupTask(context)
|
BackupCreatorJob.setupTask(context)
|
||||||
@@ -98,14 +102,17 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
// Migrate library filters to tri-state versions
|
// Migrate library filters to tri-state versions
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
fun convertBooleanPrefToTriState(key: String): Int {
|
fun convertBooleanPrefToTriState(key: String): Int {
|
||||||
val oldPrefValue = prefs.getBoolean(key, false)
|
val oldPrefValue = prefs.getBoolean(key, false)
|
||||||
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
|
||||||
@@ -122,7 +129,7 @@ object Migrations {
|
|||||||
remove("pref_filter_completed_key")
|
remove("pref_filter_completed_key")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 53) {
|
if (oldVersion < 54) {
|
||||||
// Force MAL log out due to login flow change
|
// Force MAL log out due to login flow change
|
||||||
// v52: switched from scraping to WebView
|
// v52: switched from scraping to WebView
|
||||||
// v53: switched from WebView to OAuth
|
// v53: switched from WebView to OAuth
|
||||||
@@ -134,7 +141,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 57) {
|
if (oldVersion < 57) {
|
||||||
// Migrate DNS over HTTPS setting
|
// Migrate DNS over HTTPS setting
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||||
if (wasDohEnabled) {
|
if (wasDohEnabled) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -145,7 +151,6 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 59) {
|
if (oldVersion < 59) {
|
||||||
// Reset rotation to Free after replacing Lock
|
// Reset rotation to Free after replacing Lock
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
if (prefs.contains("pref_rotation_type_key")) {
|
if (prefs.contains("pref_rotation_type_key")) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putInt("pref_rotation_type_key", 1)
|
putInt("pref_rotation_type_key", 1)
|
||||||
@@ -154,12 +159,16 @@ object Migrations {
|
|||||||
|
|
||||||
// Disable update check for Android 5.x users
|
// Disable update check for Android 5.x users
|
||||||
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
UpdaterJob.cancelTask(context)
|
AppUpdateJob.cancelTask(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 60) {
|
if (oldVersion < 60) {
|
||||||
|
// Re-enable update check that was prevously accidentally disabled for M
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
||||||
|
AppUpdateJob.setupTask(context)
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate Rotation and Viewer values to default values for viewer_flags
|
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||||
1 -> OrientationType.FREE.flagValue
|
1 -> OrientationType.FREE.flagValue
|
||||||
2 -> OrientationType.PORTRAIT.flagValue
|
2 -> OrientationType.PORTRAIT.flagValue
|
||||||
@@ -187,6 +196,58 @@ object Migrations {
|
|||||||
LibraryUpdateJob.setupTask(context, 3)
|
LibraryUpdateJob.setupTask(context, 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 64) {
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val newSortingMode = when (oldSortingMode) {
|
||||||
|
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||||
|
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||||
|
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||||
|
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||||
|
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
|
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||||
|
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||||
|
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||||
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
val newSortingDirection = when (oldSortingDirection) {
|
||||||
|
true -> SortDirectionSetting.ASCENDING
|
||||||
|
else -> SortDirectionSetting.DESCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(PreferenceKeys.librarySortingMode)
|
||||||
|
remove(PreferenceKeys.librarySortingDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putString(PreferenceKeys.librarySortingMode, newSortingMode.name)
|
||||||
|
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 70) {
|
||||||
|
if (preferences.enabledLanguages().isSet()) {
|
||||||
|
preferences.enabledLanguages() += "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 71) {
|
||||||
|
// Handle removed every 3, 4, 6, and 8 hour library updates
|
||||||
|
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||||
|
if (updateInterval in listOf(3, 4, 6, 8)) {
|
||||||
|
preferences.libraryUpdateInterval().set(12)
|
||||||
|
LibraryUpdateJob.setupTask(context, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 72) {
|
||||||
|
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
|
||||||
|
if (!oldUpdateOngoingOnly) {
|
||||||
|
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Nsfw
|
|
||||||
@@ -28,7 +28,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
|||||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ abstract class AbstractBackupRestoreValidator {
|
|||||||
|
|
||||||
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ValidatorParseException(e: Exception) : RuntimeException(e)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import androidx.work.Worker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -24,6 +26,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,10 +139,12 @@ class BackupNotifier(private val context: Context) {
|
|||||||
val destFile = File(path, file)
|
val destFile = File(path, file)
|
||||||
val uri = destFile.getUriCompat(context)
|
val uri = destFile.getUriCompat(context)
|
||||||
|
|
||||||
|
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
|
setContentIntent(errorLogIntent)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_show_errors),
|
context.getString(R.string.action_show_errors),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
errorLogIntent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
|
|||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores backup.
|
* Restores backup.
|
||||||
@@ -96,7 +97,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
ioScope?.cancel()
|
ioScope.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,7 @@ class BackupRestoreService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
backupRestore?.writeErrorLog()
|
backupRestore?.writeErrorLog()
|
||||||
|
|
||||||
notifier.showRestoreError(exception.message)
|
notifier.showRestoreError(exception.message)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
import exh.savedsearches.JsonSavedSearch
|
import exh.savedsearches.JsonSavedSearch
|
||||||
@@ -45,10 +46,10 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
@@ -61,7 +62,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
@@ -75,13 +76,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga),
|
backupExtensionInfo(databaseManga),
|
||||||
backupSavedSearches()
|
backupSavedSearches()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
val file: UniFile = (
|
file = (
|
||||||
if (isJob) {
|
if (isJob) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
@@ -106,9 +109,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
return file.uri.toString()
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
FullBackupRestoreValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
|
file?.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,8 +156,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
* @return list of [BackupSavedSearch] to be backed up
|
* @return list of [BackupSavedSearch] to be backed up
|
||||||
*/
|
*/
|
||||||
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||||
return preferences.savedSearches().get().map {
|
return preferences.savedSearches().get().mapNotNull {
|
||||||
val sourceId = it.substringBefore(':').toLong()
|
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
BackupSavedSearch(
|
BackupSavedSearch(
|
||||||
content.name,
|
content.name,
|
||||||
@@ -180,8 +189,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = sourceManager.get(manga.source)?.getMainSource()
|
val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>()
|
||||||
if (source is MetadataSource<*, *>) {
|
if (source != null) {
|
||||||
manga.id?.let { mangaId ->
|
manga.id?.let { mangaId ->
|
||||||
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata ->
|
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata ->
|
||||||
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
|
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
|
||||||
@@ -414,9 +423,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||||
val currentSavedSearches = preferences.savedSearches().get().map {
|
val currentSavedSearches = preferences.savedSearches().get().mapNotNull {
|
||||||
val sourceId = it.substringBefore(':').toLong()
|
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = try {
|
||||||
|
Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
BackupSavedSearch(
|
BackupSavedSearch(
|
||||||
content.name,
|
content.name,
|
||||||
content.query,
|
content.query,
|
||||||
@@ -425,22 +438,19 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.savedSearches()
|
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
|
||||||
.set(
|
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
|
||||||
(
|
}.map {
|
||||||
backupSavedSearches.filter { backupSavedSearch -> currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } }
|
"${it.source}:" + Json.encodeToString(
|
||||||
.map {
|
JsonSavedSearch(
|
||||||
"${it.source}:" + Json.encodeToString(
|
it.name,
|
||||||
JsonSavedSearch(
|
it.query,
|
||||||
it.name,
|
Json.decodeFromString(it.filterList)
|
||||||
it.query,
|
)
|
||||||
Json.decodeFromString(it.filterList)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} + preferences.savedSearches().get()
|
|
||||||
)
|
|
||||||
.toSet()
|
|
||||||
)
|
)
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
preferences.savedSearches().set(newSavedSearches + preferences.savedSearches().get())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
|||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupMergedMangaReference
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupMergedMangaReference
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -48,7 +49,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
|
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
|
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
|
||||||
backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach {
|
backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach {
|
||||||
@@ -86,7 +88,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
|||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.history
|
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
// SY -->
|
// SY -->
|
||||||
val mergedMangaReferences = backupManga.mergedMangaReferences
|
val mergedMangaReferences = backupManga.mergedMangaReferences
|
||||||
|
|||||||
+11
-3
@@ -4,12 +4,14 @@ import android.content.Context
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
|
|
||||||
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@@ -19,14 +21,20 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val backupManager = FullBackupManager(context)
|
val backupManager = FullBackupManager(context)
|
||||||
|
|
||||||
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
|
val backup = try {
|
||||||
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
val backupString =
|
||||||
|
context.contentResolver.openInputStream(uri)!!.source().gzip().buffer()
|
||||||
|
.use { it.readByteArray() }
|
||||||
|
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
if (backup.backupManga.isEmpty()) {
|
if (backup.backupManga.isEmpty()) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
val sources = backup.backupSources.associate { it.sourceId to it.name }
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ data class Backup(
|
|||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
// Bump by 100 to specify this is a 0.x value
|
||||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList()
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupHistory(
|
data class BrokenBackupHistory(
|
||||||
@ProtoNumber(0) var url: String,
|
@ProtoNumber(0) var url: String,
|
||||||
@ProtoNumber(1) var lastRead: Long
|
@ProtoNumber(1) var lastRead: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var lastRead: Long
|
||||||
|
)
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ data class BackupManga(
|
|||||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||||
@ProtoNumber(103) var viewer_flags: Int? = null,
|
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||||
|
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
|
||||||
|
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
|
||||||
@@ -46,8 +47,9 @@ data class BackupManga(
|
|||||||
@ProtoNumber(800) var customTitle: String? = null,
|
@ProtoNumber(800) var customTitle: String? = null,
|
||||||
@ProtoNumber(801) var customArtist: String? = null,
|
@ProtoNumber(801) var customArtist: String? = null,
|
||||||
@ProtoNumber(802) var customAuthor: String? = null,
|
@ProtoNumber(802) var customAuthor: String? = null,
|
||||||
@ProtoNumber(803) var customDescription: String? = null,
|
// skipping 803 due to using duplicate value in previous builds
|
||||||
@ProtoNumber(803) var customGenre: List<String>? = null,
|
@ProtoNumber(804) var customDescription: String? = null,
|
||||||
|
@ProtoNumber(805) var customGenre: List<String>? = null,
|
||||||
|
|
||||||
// Neko specific values
|
// Neko specific values
|
||||||
@ProtoNumber(901) var filtered_scanlators: String? = null,
|
@ProtoNumber(901) var filtered_scanlators: String? = null,
|
||||||
|
|||||||
@@ -5,9 +5,15 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long
|
@ProtoNumber(1) var sourceId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(1) var name: String = "",
|
||||||
|
@ProtoNumber(2) var sourceId: Long
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(source: Source): BackupSource {
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ data class BackupTracking(
|
|||||||
media_id = this@BackupTracking.mediaId
|
media_id = this@BackupTracking.mediaId
|
||||||
library_id = this@BackupTracking.libraryId
|
library_id = this@BackupTracking.libraryId
|
||||||
title = this@BackupTracking.title
|
title = this@BackupTracking.title
|
||||||
// convert from float to int because of 1.x types
|
last_chapter_read = this@BackupTracking.lastChapterRead
|
||||||
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
|
||||||
total_chapters = this@BackupTracking.totalChapters
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
score = this@BackupTracking.score
|
score = this@BackupTracking.score
|
||||||
status = this@BackupTracking.status
|
status = this@BackupTracking.status
|
||||||
@@ -51,8 +50,7 @@ data class BackupTracking(
|
|||||||
// forced not null so its compatible with 1.x backup system
|
// forced not null so its compatible with 1.x backup system
|
||||||
libraryId = track.library_id!!,
|
libraryId = track.library_id!!,
|
||||||
title = track.title,
|
title = track.title,
|
||||||
// convert to float for 1.x
|
lastChapterRead = track.last_chapter_read,
|
||||||
lastChapterRead = track.last_chapter_read.toFloat(),
|
|
||||||
totalChapters = track.total_chapters,
|
totalChapters = track.total_chapters,
|
||||||
score = track.score,
|
score = track.score,
|
||||||
status = track.status,
|
status = track.status,
|
||||||
|
|||||||
@@ -2,32 +2,26 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MergedMangaReferenceTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MergedMangaTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
@@ -39,23 +33,33 @@ import exh.source.MERGED_SOURCE_ID
|
|||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import timber.log.Timber
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import java.lang.RuntimeException
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser: Gson = when (version) {
|
val parser: Json = when (version) {
|
||||||
2 -> GsonBuilder()
|
2 -> Json {
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
// Forks may have added items to backup
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
ignoreUnknownKeys = true
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
// Register custom serializers
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
serializersModule = SerializersModule {
|
||||||
// SY -->
|
contextual(MangaTypeSerializer)
|
||||||
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
|
contextual(MangaImplTypeSerializer)
|
||||||
// SY <--
|
contextual(ChapterTypeSerializer)
|
||||||
.create()
|
contextual(ChapterImplTypeSerializer)
|
||||||
|
contextual(CategoryTypeSerializer)
|
||||||
|
contextual(CategoryImplTypeSerializer)
|
||||||
|
contextual(TrackTypeSerializer)
|
||||||
|
contextual(TrackImplTypeSerializer)
|
||||||
|
contextual(HistoryTypeSerializer)
|
||||||
|
// SY -->
|
||||||
|
contextual(MergedMangaTypeSerializer)
|
||||||
|
// SY <--
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +120,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param jsonCategories array containing categories
|
* @param backupCategories array containing categories
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
internal fun restoreCategories(backupCategories: List<Category>) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
@@ -281,58 +284,47 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
internal fun restoreSavedSearches(jsonSavedSearches: JsonElement) {
|
internal fun restoreSavedSearches(jsonSavedSearches: String) {
|
||||||
val backupSavedSearches = jsonSavedSearches.asString.split("***").toSet()
|
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
|
||||||
|
|
||||||
val newSavedSearches = backupSavedSearches.mapNotNull {
|
val newSavedSearches = backupSavedSearches.mapNotNull {
|
||||||
try {
|
runCatching {
|
||||||
val id = it.substringBefore(':').toLong()
|
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
id to content
|
id to content
|
||||||
} catch (t: RuntimeException) {
|
}.getOrNull()
|
||||||
// Load failed
|
}.toMutableSet()
|
||||||
Timber.e(t, "Failed to load saved search!")
|
|
||||||
t.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
val currentSources = newSavedSearches.map { it.first }.toSet()
|
val currentSources = newSavedSearches.map(Pair<Long, *>::first).toSet()
|
||||||
|
|
||||||
newSavedSearches += preferences.savedSearches().get().mapNotNull {
|
newSavedSearches += preferences.savedSearches().get().mapNotNull {
|
||||||
try {
|
kotlin.runCatching {
|
||||||
val id = it.substringBefore(':').toLong()
|
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
|
||||||
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
|
||||||
id to content
|
id to content
|
||||||
} catch (t: RuntimeException) {
|
}.getOrNull()
|
||||||
// Load failed
|
}
|
||||||
Timber.e(t, "Failed to load saved search!")
|
|
||||||
t.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
val otherSerialized = preferences.savedSearches().get().mapNotNull {
|
val otherSerialized = preferences.savedSearches().get().mapNotNull {
|
||||||
val sourceId = it.split(":")[0].toLongOrNull() ?: return@mapNotNull null
|
val sourceId = it.substringBefore(":").toLongOrNull() ?: return@mapNotNull null
|
||||||
if (sourceId in currentSources) return@mapNotNull null
|
if (sourceId in currentSources) return@mapNotNull null
|
||||||
it
|
it
|
||||||
}
|
}.toSet()
|
||||||
|
|
||||||
val newSerialized = newSavedSearches.map {
|
val newSerialized = newSavedSearches.map { (source, savedSearch) ->
|
||||||
"${it.first}:" + Json.encodeToString(it.second)
|
"$source:" + Json.encodeToString(savedSearch)
|
||||||
}
|
}.toSet()
|
||||||
preferences.savedSearches().set((otherSerialized + newSerialized).toSet())
|
preferences.savedSearches().set(otherSerialized + newSerialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param jsonMergedMangaReferences array containing md manga references
|
* @param backupMergedMangaReferences array containing md manga references
|
||||||
*/
|
*/
|
||||||
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
|
internal fun restoreMergedMangaReferences(backupMergedMangaReferences: List<MergedMangaReference>) {
|
||||||
// Get merged manga references from file and from db
|
// Get merged manga references from file and from db
|
||||||
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
|
||||||
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
|
|
||||||
var lastMergeManga: Manga? = null
|
var lastMergeManga: Manga? = null
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import exh.EXHMigrations
|
import exh.EXHMigrations
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
@@ -30,59 +30,59 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
// SY -->
|
// SY -->
|
||||||
throttleManager.resetThrottle()
|
throttleManager.resetThrottle()
|
||||||
// SY <--
|
// SY <--
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
// Read the json and create a Json Object,
|
||||||
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
|
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||||
|
context.contentResolver.openInputStream(uri)!!
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
backupManager = LegacyBackupManager(context, version)
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
// Decode the json object to a Backup object
|
||||||
restoreAmount = mangasJson.size() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||||
|
|
||||||
// Restore categories
|
restoreAmount = backup.mangas.size + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
|
||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
json.get(Backup.SAVEDSEARCHES)?.let { restoreSavedSearches(it) }
|
backup.savedSearches?.let { restoreSavedSearches(it) }
|
||||||
|
|
||||||
json.get(Backup.MERGEDMANGAREFERENCES)?.let { restoreMergedMangaReferences(it) }
|
backup.mergedMangaReferences?.let { restoreMergedMangaReferences(it) }
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
backup.categories?.let { restoreCategories(it) }
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
backup.mangas.forEach {
|
||||||
if (job?.isActive != true) {
|
if (job?.isActive != true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
restoreManga(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
|
||||||
db.inTransaction {
|
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreProgress += 1
|
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun restoreSavedSearches(savedSearchesJson: JsonElement) {
|
private fun restoreSavedSearches(savedSearches: String) {
|
||||||
backupManager.restoreSavedSearches(savedSearchesJson)
|
backupManager.restoreSavedSearches(savedSearches)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.saved_searches))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.saved_searches))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
|
private fun restoreMergedMangaReferences(mergedMangaReferences: List<MergedMangaReference>) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
|
backupManager.restoreMergedMangaReferences(mergedMangaReferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
@@ -90,28 +90,21 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
db.inTransaction {
|
||||||
mangaJson.get(
|
backupManager.restoreCategories(categoriesJson)
|
||||||
Backup.MANGA
|
}
|
||||||
)
|
|
||||||
)
|
restoreProgress += 1
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
mangaJson.get(Backup.CHAPTERS)
|
}
|
||||||
?: JsonArray()
|
|
||||||
)
|
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
val manga = mangaJson.manga
|
||||||
mangaJson.get(Backup.CATEGORIES)
|
val chapters = mangaJson.chapters ?: emptyList()
|
||||||
?: JsonArray()
|
val categories = mangaJson.categories ?: emptyList()
|
||||||
)
|
val history = mangaJson.history ?: emptyList()
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
val tracks = mangaJson.track ?: emptyList()
|
||||||
mangaJson.get(Backup.HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(Backup.TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
// EXH -->
|
// EXH -->
|
||||||
EXHMigrations.migrateBackupEntry(manga)
|
EXHMigrations.migrateBackupEntry(manga)
|
||||||
|
|||||||
+24
-24
@@ -2,14 +2,14 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for critical backup file data.
|
* Checks for critical backup file data.
|
||||||
*
|
*
|
||||||
@@ -17,30 +17,34 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val backupManager = LegacyBackupManager(context)
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)
|
val backup = try {
|
||||||
val mangasJson = json.get(Backup.MANGAS)
|
backupManager.parser.decodeFromStream<Backup>(
|
||||||
if (version == null || mangasJson == null) {
|
context.contentResolver.openInputStream(uri)!!
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw ValidatorParseException(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = mangasJson.asJsonArray
|
if (backup.mangas.isEmpty()) {
|
||||||
if (mangas.size() == 0) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = getSourceMapping(json)
|
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
val trackers = mangas
|
val trackers = backup.mangas
|
||||||
.filter { it.asJsonObject.has("track") }
|
.filterNot { it.track.isNullOrEmpty() }
|
||||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
.flatMap { it.track ?: emptyList() }
|
||||||
.map { it.asJsonObject["s"].asInt }
|
.map { it.sync_id }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
@@ -52,15 +56,11 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
return extensionsMapping.associate {
|
||||||
|
val items = it.split(":")
|
||||||
return extensionsMapping.asJsonArray
|
items[0].toLong() to items[1]
|
||||||
.map {
|
}
|
||||||
val items = it.asString.split(":")
|
|
||||||
items[0].toLong() to items[1]
|
|
||||||
}
|
|
||||||
.toMap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,43 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
@Serializable
|
||||||
* Json values
|
data class Backup(
|
||||||
*/
|
val version: Int? = null,
|
||||||
object Backup {
|
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||||
const val CURRENT_VERSION = 2
|
var categories: List<@Contextual Category>? = null,
|
||||||
const val MANGA = "manga"
|
var extensions: List<String>? = null,
|
||||||
const val MANGAS = "mangas"
|
// SY Specific values
|
||||||
const val TRACK = "track"
|
@SerialName("mergedmangareferences")
|
||||||
const val CHAPTERS = "chapters"
|
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
|
||||||
const val CATEGORIES = "categories"
|
var savedSearches: String? = null
|
||||||
const val EXTENSIONS = "extensions"
|
) {
|
||||||
const val HISTORY = "history"
|
companion object {
|
||||||
const val VERSION = "version"
|
const val CURRENT_VERSION = 2
|
||||||
|
|
||||||
// SY -->
|
fun getDefaultFilename(): String {
|
||||||
const val SAVEDSEARCHES = "savedsearches"
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
|
return "tachiyomi_$date.json"
|
||||||
// SY <--
|
}
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
|
||||||
return "tachiyomi_$date.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaObject(
|
||||||
|
var manga: @Contextual Manga,
|
||||||
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
|
var categories: List<String>? = null,
|
||||||
|
var track: List<@Contextual Track>? = null,
|
||||||
|
var history: List<@Contextual DHistory>? = null
|
||||||
|
)
|
||||||
|
|||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
object CategoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<CategoryImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.name)
|
|
||||||
value(it.order)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val category = CategoryImpl()
|
|
||||||
category.name = nextString()
|
|
||||||
category.order = nextInt()
|
|
||||||
endArray()
|
|
||||||
category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.name)
|
||||||
|
add(value.order)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a category impl and cast as T so that the serializer accepts it
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
name = array[0].jsonPrimitive.content
|
||||||
|
order = array[1].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a category and category impl
|
||||||
|
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
||||||
|
|
||||||
|
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
object ChapterTypeAdapter {
|
|
||||||
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<ChapterImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
|
||||||
beginObject()
|
|
||||||
name(URL)
|
|
||||||
value(it.url)
|
|
||||||
if (it.read) {
|
|
||||||
name(READ)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.bookmark) {
|
|
||||||
name(BOOKMARK)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.last_page_read != 0) {
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_page_read)
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
||||||
|
|
||||||
|
override val descriptor = buildClassSerialDescriptor("Chapter")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(URL, value.url)
|
||||||
|
if (value.read) {
|
||||||
|
put(READ, 1)
|
||||||
|
}
|
||||||
|
if (value.bookmark) {
|
||||||
|
put(BOOKMARK, 1)
|
||||||
|
}
|
||||||
|
if (value.last_page_read != 0) {
|
||||||
|
put(LAST_READ, value.last_page_read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a chapter impl and cast as T so that the serializer accepts it
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
url = jsonObject[URL]!!.jsonPrimitive.content
|
||||||
|
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a chapter and chapter impl
|
||||||
|
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
||||||
|
|
||||||
|
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<DHistory> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.lastRead != 0L) {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.lastRead)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val url = nextString()
|
|
||||||
val lastRead = nextLong()
|
|
||||||
endArray()
|
|
||||||
DHistory(url, lastRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: DHistory) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.lastRead)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): DHistory {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
return DHistory(
|
||||||
|
url = array[0].jsonPrimitive.content,
|
||||||
|
lastRead = array[1].jsonPrimitive.long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
object MangaTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MangaImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
// SY -->
|
|
||||||
value(it.originalTitle)
|
|
||||||
// SY <--
|
|
||||||
value(it.source)
|
|
||||||
value(it.viewer_flags)
|
|
||||||
value(it.chapter_flags)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.url = nextString()
|
|
||||||
manga.title = nextString()
|
|
||||||
manga.source = nextLong()
|
|
||||||
manga.viewer_flags = nextInt()
|
|
||||||
manga.chapter_flags = nextInt()
|
|
||||||
endArray()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.title)
|
||||||
|
add(value.source)
|
||||||
|
add(value.viewer_flags)
|
||||||
|
add(value.chapter_flags)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a manga impl and cast as T so that the serializer accepts it
|
||||||
|
return MangaImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
url = array[0].jsonPrimitive.content
|
||||||
|
title = array[1].jsonPrimitive.content
|
||||||
|
source = array[2].jsonPrimitive.long
|
||||||
|
viewer_flags = array[3].jsonPrimitive.int
|
||||||
|
chapter_flags = array[4].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a manga and manga impl
|
||||||
|
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
||||||
|
|
||||||
|
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
||||||
-45
@@ -1,45 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
|
||||||
*/
|
|
||||||
object MergedMangaReferenceTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MergedMangaReference> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.mangaUrl)
|
|
||||||
value(it.mergeUrl)
|
|
||||||
value(it.mangaSourceId)
|
|
||||||
value(it.chapterSortMode)
|
|
||||||
value(it.chapterPriority)
|
|
||||||
value(it.getChapterUpdates)
|
|
||||||
value(it.isInfoManga)
|
|
||||||
value(it.downloadChapters)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
MergedMangaReference(
|
|
||||||
id = null,
|
|
||||||
mangaUrl = nextString(),
|
|
||||||
mergeUrl = nextString(),
|
|
||||||
mangaSourceId = nextLong(),
|
|
||||||
chapterSortMode = nextInt(),
|
|
||||||
chapterPriority = nextInt(),
|
|
||||||
getChapterUpdates = nextBoolean(),
|
|
||||||
isInfoManga = nextBoolean(),
|
|
||||||
downloadChapters = nextBoolean(),
|
|
||||||
mangaId = null,
|
|
||||||
mergeId = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MergedMangaReference] to / from json
|
||||||
|
*/
|
||||||
|
object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: MergedMangaReference) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.mangaUrl)
|
||||||
|
add(value.mergeUrl)
|
||||||
|
add(value.mangaSourceId)
|
||||||
|
add(value.chapterSortMode)
|
||||||
|
add(value.chapterPriority)
|
||||||
|
add(value.getChapterUpdates)
|
||||||
|
add(value.isInfoManga)
|
||||||
|
add(value.downloadChapters)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): MergedMangaReference {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
return MergedMangaReference(
|
||||||
|
id = null,
|
||||||
|
mangaUrl = array[0].jsonPrimitive.content,
|
||||||
|
mergeUrl = array[1].jsonPrimitive.content,
|
||||||
|
mangaSourceId = array[2].jsonPrimitive.long,
|
||||||
|
chapterSortMode = array[3].jsonPrimitive.int,
|
||||||
|
chapterPriority = array[4].jsonPrimitive.int,
|
||||||
|
getChapterUpdates = array[5].jsonPrimitive.boolean,
|
||||||
|
isInfoManga = array[6].jsonPrimitive.boolean,
|
||||||
|
downloadChapters = array[7].jsonPrimitive.boolean,
|
||||||
|
mangaId = null,
|
||||||
|
mergeId = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
object TrackTypeAdapter {
|
|
||||||
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginObject()
|
|
||||||
name(TITLE)
|
|
||||||
value(it.title)
|
|
||||||
name(SYNC)
|
|
||||||
value(it.sync_id)
|
|
||||||
name(MEDIA)
|
|
||||||
value(it.media_id)
|
|
||||||
name(LIBRARY)
|
|
||||||
value(it.library_id)
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_chapter_read)
|
|
||||||
name(TRACKING_URL)
|
|
||||||
value(it.tracking_url)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val track = TrackImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
TITLE -> track.title = nextString()
|
|
||||||
SYNC -> track.sync_id = nextInt()
|
|
||||||
MEDIA -> track.media_id = nextInt()
|
|
||||||
LIBRARY -> track.library_id = nextLong()
|
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(TITLE, value.title)
|
||||||
|
put(SYNC, value.sync_id)
|
||||||
|
put(MEDIA, value.media_id)
|
||||||
|
put(LIBRARY, value.library_id)
|
||||||
|
put(LAST_READ, value.last_chapter_read)
|
||||||
|
put(TRACKING_URL, value.tracking_url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a track impl and cast as T so that the serializer accepts it
|
||||||
|
return TrackImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
||||||
|
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||||
|
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||||
|
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||||
|
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
|
||||||
|
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a track and track impl
|
||||||
|
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
||||||
|
|
||||||
|
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
||||||
@@ -10,6 +10,7 @@ import coil.network.HttpException
|
|||||||
import coil.request.get
|
import coil.request.get
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
@@ -27,7 +28,6 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||||
@@ -62,14 +62,15 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||||
val coverFile = coverCache.getCoverFile(manga) ?: error("No cover specified")
|
// Only cache separately if it's a library item
|
||||||
|
val coverCacheFile = if (manga.favorite) {
|
||||||
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
// Use previously cached cover if exist
|
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
if (coverFile.exists() && options.diskCachePolicy.readEnabled) {
|
return fileLoader(coverCacheFile)
|
||||||
if (!manga.favorite) {
|
|
||||||
coverFile.setLastModified(Date().time)
|
|
||||||
}
|
|
||||||
return fileLoader(coverFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val (response, body) = awaitGetCall(manga, options)
|
val (response, body) = awaitGetCall(manga, options)
|
||||||
@@ -78,18 +79,16 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
throw HttpException(response)
|
throw HttpException(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to disk for future use
|
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||||
if (options.diskCachePolicy.writeEnabled) {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
val tmpFile = File(coverFile.absolutePath + "_tmp")
|
coverCacheFile.parentFile?.mkdirs()
|
||||||
tmpFile.parentFile?.mkdirs()
|
if (coverCacheFile.exists()) {
|
||||||
tmpFile.sink().buffer().use { output ->
|
coverCacheFile.delete()
|
||||||
|
}
|
||||||
|
coverCacheFile.sink().buffer().use { output ->
|
||||||
output.writeAll(input)
|
output.writeAll(input)
|
||||||
}
|
}
|
||||||
if (coverFile.exists()) {
|
|
||||||
coverFile.delete()
|
|
||||||
}
|
|
||||||
tmpFile.renameTo(coverFile)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +107,6 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
|
|
||||||
private fun getCall(manga: Manga, options: Options): Call {
|
private fun getCall(manga: Manga, options: Options): Call {
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
val client = source?.client ?: defaultClient
|
|
||||||
|
|
||||||
val newClient = client.newBuilder().build()
|
|
||||||
|
|
||||||
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
it.headers(source.headers)
|
it.headers(source.headers)
|
||||||
@@ -135,7 +130,8 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
return newClient.newCall(request)
|
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
||||||
|
return client.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fileLoader(manga: Manga): FetchResult {
|
private fun fileLoader(manga: Manga): FetchResult {
|
||||||
@@ -153,7 +149,7 @@ class MangaCoverFetcher : Fetcher<Manga> {
|
|||||||
private fun getResourceType(cover: String?): Type? {
|
private fun getResourceType(cover: String?): Type? {
|
||||||
return when {
|
return when {
|
||||||
cover.isNullOrEmpty() -> null
|
cover.isNullOrEmpty() -> null
|
||||||
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
|
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
||||||
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
|
*/
|
||||||
|
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||||
|
|
||||||
|
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||||
|
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun decode(
|
||||||
|
pool: BitmapPool,
|
||||||
|
source: BufferedSource,
|
||||||
|
size: Size,
|
||||||
|
options: Options
|
||||||
|
): DecodeResult {
|
||||||
|
val decoder = source.use {
|
||||||
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
||||||
|
|
||||||
|
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
|
return DecodeResult(
|
||||||
|
drawable = bitmap.toDrawable(resources),
|
||||||
|
isSampled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import exh.favorites.sql.mappers.FavoriteEntryTypeMapping
|
||||||
|
import exh.favorites.sql.models.FavoriteEntry
|
||||||
|
import exh.favorites.sql.queries.FavoriteEntryQueries
|
||||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.merged.sql.queries.MergedQueries
|
import exh.merged.sql.queries.MergedQueries
|
||||||
@@ -39,7 +42,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries /* SY <-- */ {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@@ -59,6 +62,7 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
|
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
|
||||||
// SY <--
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import exh.favorites.sql.tables.FavoriteEntryTable
|
||||||
import exh.merged.sql.tables.MergedTable
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
@@ -24,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */
|
const val DATABASE_VERSION = /* SY --> */ 12 /* SY <-- */
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@@ -39,6 +40,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
|
execSQL(FavoriteEntryTable.createTableQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
@@ -81,6 +83,24 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
if (oldVersion < 7) {
|
if (oldVersion < 7) {
|
||||||
db.execSQL("DROP TABLE IF EXISTS manga_related")
|
db.execSQL("DROP TABLE IF EXISTS manga_related")
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 8) {
|
||||||
|
db.execSQL(MangaTable.addNextUpdateCol)
|
||||||
|
}
|
||||||
|
if (oldVersion < 9) {
|
||||||
|
db.execSQL(TrackTable.renameTableToTemp)
|
||||||
|
db.execSQL(TrackTable.createTableQuery)
|
||||||
|
db.execSQL(TrackTable.insertFromTempTable)
|
||||||
|
db.execSQL(TrackTable.dropTempTable)
|
||||||
|
}
|
||||||
|
if (oldVersion < 10) {
|
||||||
|
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
|
||||||
|
}
|
||||||
|
if (oldVersion < 11) {
|
||||||
|
db.execSQL(FavoriteEntryTable.createTableQuery)
|
||||||
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(FavoriteEntryTable.fixTableQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
|
|||||||
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
class CategoryGetResolver : DefaultGetResolver<Category>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
|
||||||
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
|
id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
|
order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ORDER))
|
||||||
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
|
flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FLAGS))
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
|
val orderString = cursor.getString(cursor.getColumnIndexOrThrow(COL_MANGA_ORDER))
|
||||||
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() }.orEmpty()
|
mangaOrder = orderString?.split("/")?.mapNotNull { it.toLongOrNull() }.orEmpty()
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,18 +63,18 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
|
|||||||
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
|
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
|
||||||
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
|
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
|
||||||
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
|
read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_READ)) == 1
|
||||||
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
|
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
|
||||||
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
|
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
|
||||||
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
|
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
|
||||||
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
|
last_page_read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_LAST_PAGE_READ))
|
||||||
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
|
chapter_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_CHAPTER_NUMBER))
|
||||||
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
|
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
|
|||||||
class HistoryGetResolver : DefaultGetResolver<History>() {
|
class HistoryGetResolver : DefaultGetResolver<History>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
|
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
|
||||||
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
|
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
|
||||||
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
|
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -44,9 +44,9 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
|
|||||||
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
|
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,24 +76,24 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
|
|
||||||
interface BaseMangaGetResolver {
|
interface BaseMangaGetResolver {
|
||||||
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
|
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
|
||||||
url = cursor.getString(cursor.getColumnIndex(COL_URL))
|
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
|
||||||
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
|
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
|
||||||
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
|
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
|
||||||
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
|
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
|
||||||
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
|
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
|
||||||
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
|
||||||
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
|
filtered_scanlators = cursor.getString(cursor.getColumnIndexOrThrow(COL_FILTERED_SCANLATORS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,19 +65,19 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
|
|||||||
class TrackGetResolver : DefaultGetResolver<Track>() {
|
class TrackGetResolver : DefaultGetResolver<Track>() {
|
||||||
|
|
||||||
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
|
||||||
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
|
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
|
||||||
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
|
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
|
||||||
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
|
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
|
||||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
|
||||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
|
||||||
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
|
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
|
||||||
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
|
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
|
||||||
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
|
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@@ -16,12 +21,28 @@ interface Category : Serializable {
|
|||||||
var mangaOrder: List<Long>
|
var mangaOrder: List<Long>
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
flags = flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayMode: Int
|
||||||
|
get() = flags and DisplayModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, DisplayModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortMode: Int
|
||||||
|
get() = flags and SortModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortDirection: Int
|
||||||
|
get() = flags and SortDirectionSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortDirectionSetting.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
fun createDefault(context: Context): Category = create(context.getString(R.string.label_default)).apply { id = 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
@@ -34,7 +35,8 @@ interface Manga : SManga {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ open class MangaImpl : Manga {
|
|||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val customMangaManager: CustomMangaManager by injectLazy()
|
|
||||||
|
|
||||||
override var title: String
|
override var title: String
|
||||||
get() = if (favorite) {
|
get() = if (favorite) {
|
||||||
val customTitle = customMangaManager.getManga(this)?.title
|
val customTitle = customMangaManager.getManga(this)?.title
|
||||||
@@ -91,4 +89,10 @@ open class MangaImpl : Manga {
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
return url.hashCode() + id.hashCode()
|
return url.hashCode() + id.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SY -->
|
||||||
|
companion object {
|
||||||
|
private val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
data class SourceIdMangaCount(val source: Long, val count: Int)
|
||||||
@@ -16,7 +16,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var last_chapter_read: Int
|
var last_chapter_read: Float
|
||||||
|
|
||||||
var total_chapters: Int
|
var total_chapters: Int
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var last_chapter_read: Int = 0
|
override var last_chapter_read: Float = 0F
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.Queries
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
@@ -16,6 +19,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@@ -25,15 +29,6 @@ import exh.metadata.sql.tables.SearchMetadataTable
|
|||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@@ -45,17 +40,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
.listOfObjects(Manga::class.java)
|
var queryBuilder = Query.builder()
|
||||||
.withQuery(
|
.table(MangaTable.TABLE)
|
||||||
Query.builder()
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.table(MangaTable.TABLE)
|
.whereArgs(1)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(1)
|
if (sortByTitle) {
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
||||||
.build()
|
}
|
||||||
)
|
|
||||||
.prepare()
|
return db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(queryBuilder.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
@@ -79,7 +78,27 @@ interface MangaQueries : DbProvider {
|
|||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getSourceIdsWithNonLibraryManga() = db.get()
|
||||||
|
.listOfObjects(SourceIdMangaCount::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getSourceIdsWithNonLibraryMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
fun getMangas() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
Query.builder()
|
||||||
|
.table(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
|
||||||
fun getReadNotInLibraryMangas() = db.get()
|
fun getReadNotInLibraryMangas() = db.get()
|
||||||
.listOfObjects(Manga::class.java)
|
.listOfObjects(Manga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@@ -121,7 +140,7 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(manga)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateViewerFlags(manga: Manga) = db.put()
|
fun updateViewerFlags(manga: Manga) = db.put()
|
||||||
@@ -131,7 +150,7 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(manga)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
@@ -165,30 +184,32 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||||
|
|
||||||
fun deleteMangasNotInLibrary() = db.delete()
|
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
|
// SY -->
|
||||||
.where(
|
.where(
|
||||||
"""
|
"""
|
||||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
.whereArgs(0)
|
// SY <--
|
||||||
|
.whereArgs(0, *sourceIds.toTypedArray())
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
|
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where(
|
.where(
|
||||||
"""
|
"""
|
||||||
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
|
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
|
||||||
) AND ${MangaTable.COL_ID} NOT IN (
|
) AND ${MangaTable.COL_ID} NOT IN (
|
||||||
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
|
||||||
import exh.source.MERGED_SOURCE_ID
|
import exh.source.MERGED_SOURCE_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
@@ -241,3 +242,14 @@ fun getCategoriesForMangaQuery() =
|
|||||||
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
|
||||||
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
WHERE ${MangaCategory.COL_MANGA_ID} = ?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
/** Query to get the list of sources in the database that have
|
||||||
|
* non-library manga, and how many
|
||||||
|
*/
|
||||||
|
fun getSourceIdsWithNonLibraryMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.COL_SOURCE}, COUNT(*) as ${SourceIdMangaCountGetResolver.COL_COUNT}
|
||||||
|
FROM ${Manga.TABLE}
|
||||||
|
WHERE ${Manga.COL_FAVORITE} = 0
|
||||||
|
GROUP BY ${Manga.COL_SOURCE}
|
||||||
|
"""
|
||||||
|
|||||||
+2
-14
@@ -27,9 +27,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
val putResult: PutResult
|
cursor.use { putCursor ->
|
||||||
|
|
||||||
putResult = cursor.use { putCursor ->
|
|
||||||
if (putCursor.count == 0) {
|
if (putCursor.count == 0) {
|
||||||
val insertQuery = mapToInsertQuery(history)
|
val insertQuery = mapToInsertQuery(history)
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
@@ -39,25 +37,15 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
putResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates update query
|
|
||||||
* @param obj history object
|
|
||||||
*/
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
.whereArgs(obj.chapter_id)
|
.whereArgs(obj.chapter_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
private fun mapToUpdateContentValues(history: History) =
|
||||||
* Create content query
|
|
||||||
* @param history object
|
|
||||||
*/
|
|
||||||
fun mapToUpdateContentValues(history: History) =
|
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
HistoryTable.COL_LAST_READ to history.last_read
|
HistoryTable.COL_LAST_READ to history.last_read
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -16,10 +16,10 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
|
|||||||
val manga = LibraryManga()
|
val manga = LibraryManga()
|
||||||
|
|
||||||
mapBaseFromCursor(manga, cursor)
|
mapBaseFromCursor(manga, cursor)
|
||||||
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
|
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
|
||||||
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
|
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
|
||||||
// SY -->
|
// SY -->
|
||||||
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
|
manga.read = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_READ))
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
return manga
|
return manga
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
|
|||||||
val manga = mangaGetResolver.mapFromCursor(cursor)
|
val manga = mangaGetResolver.mapFromCursor(cursor)
|
||||||
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
val chapter = chapterGetResolver.mapFromCursor(cursor)
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
|
|
||||||
return MangaChapter(manga, chapter)
|
return MangaChapter(manga, chapter)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
|
|||||||
|
|
||||||
// Make certain column conflicts are dealt with
|
// Make certain column conflicts are dealt with
|
||||||
manga.id = chapter.manga_id
|
manga.id = chapter.manga_id
|
||||||
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
|
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
|
||||||
chapter.id = history.chapter_id
|
chapter.id = history.chapter_id
|
||||||
|
|
||||||
// Return result
|
// Return result
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
|
|||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
.table(MangaTable.TABLE)
|
.table(MangaTable.TABLE)
|
||||||
.where("${MangaTable.COL_FILTERED_SCANLATORS} = ?")
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
.whereArgs(manga.filtered_scanlators)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
|
|||||||
+6
-16
@@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@@ -20,21 +20,11 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
val builder = UpdateQuery.builder()
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
return if (updateAll) {
|
.whereArgs(manga.id)
|
||||||
builder
|
.build()
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
builder
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
|
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val INSTANCE = SourceIdMangaCountGetResolver()
|
||||||
|
const val COL_COUNT = "manga_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
|
||||||
|
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(MangaTable.COL_SOURCE))
|
||||||
|
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
|
||||||
|
|
||||||
|
return SourceIdMangaCount(sourceID, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,4 +62,7 @@ object ChapterTable {
|
|||||||
|
|
||||||
val addScanlator: String
|
val addScanlator: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||||
|
|
||||||
|
val fixDateUploadIfNeeded: String
|
||||||
|
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
// Not actually used anymore
|
||||||
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
@@ -63,6 +66,7 @@ object MangaTable {
|
|||||||
$COL_THUMBNAIL_URL TEXT,
|
$COL_THUMBNAIL_URL TEXT,
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
$COL_FAVORITE INTEGER NOT NULL,
|
||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
|
$COL_NEXT_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
@@ -94,6 +98,11 @@ object MangaTable {
|
|||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
|
val addNextUpdateCol: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
||||||
|
|
||||||
|
// SY -->
|
||||||
val addFilteredScanlators: String
|
val addFilteredScanlators: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ object TrackTable {
|
|||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||||
$COL_LIBRARY_ID INTEGER,
|
$COL_LIBRARY_ID INTEGER,
|
||||||
$COL_TITLE TEXT NOT NULL,
|
$COL_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
@@ -62,4 +62,19 @@ object TrackTable {
|
|||||||
|
|
||||||
val addFinishDate: String
|
val addFinishDate: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val renameTableToTemp: String
|
||||||
|
get() =
|
||||||
|
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
||||||
|
|
||||||
|
val insertFromTempTable: String
|
||||||
|
get() =
|
||||||
|
"""
|
||||||
|
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||||
|
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||||
|
|FROM ${TABLE}_tmp
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
val dropTempTable: String
|
||||||
|
get() = "DROP TABLE ${TABLE}_tmp"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class DownloadCache(
|
|||||||
if (sourceDir != null) {
|
if (sourceDir != null) {
|
||||||
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
|
||||||
if (mangaDir != null) {
|
if (mangaDir != null) {
|
||||||
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files || "$it.cbz" in mangaDir.files }
|
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -196,8 +196,6 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
} else if ("$it.cbz" in mangaDir.files) {
|
|
||||||
mangaDir.files -= "$it.cbz"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,6 +210,7 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -228,8 +227,6 @@ class DownloadCache(
|
|||||||
provider.getValidChapterDirNames(chapter).forEach {
|
provider.getValidChapterDirNames(chapter).forEach {
|
||||||
if (it in mangaDir.files) {
|
if (it in mangaDir.files) {
|
||||||
mangaDir.files -= it
|
mangaDir.files -= it
|
||||||
} else if ("$it.cbz" in mangaDir.files) {
|
|
||||||
mangaDir.files -= "$it.cbz"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@@ -13,8 +14,13 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import exh.log.xLogE
|
||||||
|
import logcat.LogPriority
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +30,10 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(private val context: Context) {
|
class DownloadManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
) {
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@@ -32,7 +41,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
|
||||||
*/
|
*/
|
||||||
private val provider = DownloadProvider(context)
|
val provider = DownloadProvider(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache of downloaded chapters.
|
* Cache of downloaded chapters.
|
||||||
@@ -95,6 +104,23 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDownloadNow(chapter: Chapter) {
|
||||||
|
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||||
|
val queue = downloader.queue.toMutableList()
|
||||||
|
queue.remove(download)
|
||||||
|
queue.add(0, download)
|
||||||
|
reorderQueue(queue)
|
||||||
|
if (isPaused()) {
|
||||||
|
if (DownloadService.isRunning(context)) {
|
||||||
|
downloader.start()
|
||||||
|
} else {
|
||||||
|
DownloadService.start(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPaused() = downloader.isPaused()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the download queue.
|
* Reorders the download queue.
|
||||||
*
|
*
|
||||||
@@ -200,7 +226,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param download the download to cancel.
|
* @param download the download to cancel.
|
||||||
*/
|
*/
|
||||||
fun deletePendingDownload(download: Download) {
|
fun deletePendingDownload(download: Download) {
|
||||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
deleteChapters(listOf(download.chapter), download.manga, download.source, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deletePendingDownloads(vararg downloads: Download) {
|
fun deletePendingDownloads(vararg downloads: Download) {
|
||||||
@@ -208,7 +234,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloadsByManga.map { entry ->
|
downloadsByManga.map { entry ->
|
||||||
val manga = entry.value.first().manga
|
val manga = entry.value.first().manga
|
||||||
val source = entry.value.first().source
|
val source = entry.value.first().source
|
||||||
deleteChapters(entry.value.map { it.chapter }, manga, source)
|
deleteChapters(entry.value.map { it.chapter }, manga, source, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,9 +244,15 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param chapters the list of chapters to delete.
|
* @param chapters the list of chapters to delete.
|
||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
|
* @param isCancelling true if it's simply cancelling a download
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, isCancelling: Boolean = false): List<Chapter> {
|
||||||
val filteredChapters = getChaptersToDelete(chapters)
|
val filteredChapters = if (isCancelling) {
|
||||||
|
chapters
|
||||||
|
} else {
|
||||||
|
getChaptersToDelete(chapters, manga)
|
||||||
|
}
|
||||||
|
|
||||||
launchIO {
|
launchIO {
|
||||||
removeFromDownloadQueue(filteredChapters)
|
removeFromDownloadQueue(filteredChapters)
|
||||||
|
|
||||||
@@ -297,7 +329,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
mangaFolder.delete()
|
mangaFolder.delete()
|
||||||
cache.removeManga(manga)
|
cache.removeManga(manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Cache and download folder doesn't match for %s", manga.title)
|
xLogE("Cache and download folder doesn't match for " + manga.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cleaned
|
return cleaned
|
||||||
@@ -325,7 +357,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,27 +381,46 @@ class DownloadManager(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
|
||||||
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
val oldNames = provider.getValidChapterDirNames(oldChapter)
|
||||||
val newName = provider.getChapterDirName(newChapter)
|
|
||||||
val mangaDir = provider.getMangaDir(manga, source)
|
val mangaDir = provider.getMangaDir(manga, source)
|
||||||
|
|
||||||
// Assume there's only 1 version of the chapter name formats present
|
// Assume there's only 1 version of the chapter name formats present
|
||||||
val oldFolder = oldNames.asSequence()
|
val oldDownload = oldNames.asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull() ?: return
|
||||||
|
|
||||||
if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
|
var newName = provider.getChapterDirName(newChapter)
|
||||||
|
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
|
||||||
|
newName += ".cbz"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldDownload.renameTo(newName)) {
|
||||||
cache.removeChapter(oldChapter, manga)
|
cache.removeChapter(oldChapter, manga)
|
||||||
cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
|
cache.addChapter(newName, mangaDir, manga)
|
||||||
} else {
|
} else {
|
||||||
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
|
logcat(LogPriority.ERROR) { "Could not rename downloaded chapter: ${oldNames.joinToString()}." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||||
return if (!preferences.removeBookmarkedChapters()) {
|
// Retrieve the categories that are set to exclude from being deleted on read
|
||||||
|
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
|
||||||
|
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
.mapNotNull { it.id }
|
||||||
|
.takeUnless { it.isEmpty() }
|
||||||
|
?: listOf(0)
|
||||||
|
|
||||||
|
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
|
||||||
|
chapters.filterNot { it.read }
|
||||||
|
} else if (!preferences.removeBookmarkedChapters()) {
|
||||||
chapters.filterNot { it.bookmark }
|
chapters.filterNot { it.bookmark }
|
||||||
} else {
|
} else {
|
||||||
chapters
|
chapters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun renameMangaDir(oldTitle: String, newTitle: String, source: Long) {
|
||||||
|
val sourceDir = provider.findSourceDir(sourceManager.getOrStub(source)) ?: return
|
||||||
|
val mangaDir = sourceDir.findFile(DiskUtil.buildValidFilename(oldTitle), true) ?: return
|
||||||
|
mangaDir.renameTo(DiskUtil.buildValidFilename(newTitle))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
var errorThrown = false
|
private var errorThrown = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
@@ -105,6 +105,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
|
|
||||||
if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
setContentTitle(downloadingProgressText)
|
setContentTitle(downloadingProgressText)
|
||||||
|
setContentText(null)
|
||||||
} else {
|
} else {
|
||||||
val title = download.manga.title.chop(15)
|
val title = download.manga.title.chop(15)
|
||||||
val quotedTitle = Pattern.quote(title)
|
val quotedTitle = Pattern.quote(title)
|
||||||
@@ -187,8 +188,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
fun onWarning(reason: String) {
|
fun onWarning(reason: String) {
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
|
||||||
setContentText(reason)
|
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@@ -208,15 +209,14 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
* @param error string containing error information.
|
* @param error string containing error information.
|
||||||
* @param chapter string containing chapter title.
|
* @param chapter string containing chapter title.
|
||||||
*/
|
*/
|
||||||
fun onError(error: String? = null, chapter: String? = null) {
|
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
|
||||||
// Create notification
|
// Create notification
|
||||||
with(errorNotificationBuilder) {
|
with(errorNotificationBuilder) {
|
||||||
setContentTitle(
|
setContentTitle(
|
||||||
chapter
|
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
|
||||||
?: context.getString(R.string.download_notifier_downloader_title)
|
|
||||||
)
|
)
|
||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(R.drawable.ic_warning_white_24dp)
|
||||||
clearActions()
|
clearActions()
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.MainScope
|
import kotlinx.coroutines.MainScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +55,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "Invalid download directory")
|
logcat(LogPriority.ERROR, e) { "Invalid download directory" }
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return getValidChapterDirNames(chapter).asSequence()
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
|
.mapNotNull { mangaDir?.findFile(it, true) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||||
return chapters.mapNotNull { chapter ->
|
return chapters.mapNotNull { chapter ->
|
||||||
getValidChapterDirNames(chapter).asSequence()
|
getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
|
.mapNotNull { mangaDir.findFile(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return mangaDir.listFiles().orEmpty().asList().filter {
|
return mangaDir.listFiles().orEmpty().asList().filter {
|
||||||
chapters.find { chp ->
|
chapters.find { chp ->
|
||||||
getValidChapterDirNames(chp).any { dir ->
|
getValidChapterDirNames(chp).any { dir ->
|
||||||
mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
|
mangaDir.findFile(dir) != null
|
||||||
}
|
}
|
||||||
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
} == null || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
|
||||||
}
|
}
|
||||||
@@ -173,10 +174,14 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param chapter the chapter to query.
|
* @param chapter the chapter to query.
|
||||||
*/
|
*/
|
||||||
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
fun getValidChapterDirNames(chapter: Chapter): List<String> {
|
||||||
|
val chapterName = getChapterDirName(chapter)
|
||||||
return listOf(
|
return listOf(
|
||||||
getChapterDirName(chapter),
|
// Folder of images
|
||||||
|
chapterName,
|
||||||
|
|
||||||
|
// Archived chapters
|
||||||
|
"$chapterName.cbz",
|
||||||
|
|
||||||
// TODO: remove this
|
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,25 +4,32 @@ import android.app.Notification
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import logcat.LogPriority
|
||||||
|
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -58,6 +65,16 @@ class DownloadService : Service() {
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, DownloadService::class.java))
|
context.stopService(Intent(context, DownloadService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return context.isServiceRunning(DownloadService::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
@@ -69,16 +86,15 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscriptions to store while the service is running.
|
|
||||||
*/
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the service is created.
|
* Called when the service is created.
|
||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
@@ -91,6 +107,7 @@ class DownloadService : Service() {
|
|||||||
* Called when the service is destroyed.
|
* Called when the service is destroyed.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
ioScope?.cancel()
|
||||||
runningRelay.call(false)
|
runningRelay.call(false)
|
||||||
subscriptions.unsubscribe()
|
subscriptions.unsubscribe()
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads()
|
||||||
@@ -118,55 +135,56 @@ class DownloadService : Service() {
|
|||||||
* @see onNetworkStateChanged
|
* @see onNetworkStateChanged
|
||||||
*/
|
*/
|
||||||
private fun listenNetworkChanges() {
|
private fun listenNetworkChanges() {
|
||||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
ReactiveNetwork()
|
||||||
.subscribeOn(Schedulers.io())
|
.observeNetworkConnectivity(applicationContext)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.onEach {
|
||||||
.subscribe(
|
withUIContext {
|
||||||
{ state ->
|
onNetworkStateChanged()
|
||||||
onNetworkStateChanged(state)
|
}
|
||||||
},
|
}
|
||||||
{
|
.catch { error ->
|
||||||
|
withUIContext {
|
||||||
|
logcat(LogPriority.ERROR, error)
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the network state changes.
|
* Called when the network state changes.
|
||||||
*
|
|
||||||
* @param connectivity the new network state.
|
|
||||||
*/
|
*/
|
||||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
private fun onNetworkStateChanged() {
|
||||||
when (connectivity.state) {
|
if (isOnline()) {
|
||||||
CONNECTED -> {
|
if (preferences.downloadOnlyOverWifi() && !isConnectedToWifi()) {
|
||||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
stopDownloads(R.string.download_notifier_text_only_wifi)
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
} else {
|
||||||
} else {
|
val started = downloadManager.startDownloads()
|
||||||
val started = downloadManager.startDownloads()
|
if (!started) stopSelf()
|
||||||
if (!started) stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DISCONNECTED -> {
|
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
/* Do nothing */
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
stopDownloads(R.string.download_notifier_no_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopDownloads(@StringRes string: Int) {
|
||||||
|
downloadManager.stopDownloads(getString(string))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
||||||
*/
|
*/
|
||||||
private fun listenDownloaderState() {
|
private fun listenDownloaderState() {
|
||||||
subscriptions += downloadManager.runningRelay.subscribe { running ->
|
subscriptions += downloadManager.runningRelay
|
||||||
if (running) {
|
.doOnError { /* Swallow wakelock error */ }
|
||||||
wakeLock.acquireIfNeeded()
|
.subscribe { running ->
|
||||||
} else {
|
if (running) {
|
||||||
wakeLock.releaseIfNeeded()
|
wakeLock.acquireIfNeeded()
|
||||||
|
} else {
|
||||||
|
wakeLock.releaseIfNeeded()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
|||||||
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
|
||||||
@@ -23,13 +24,16 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import exh.util.DataSaver
|
||||||
|
import exh.util.DataSaver.Companion.fetchImage
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import logcat.LogPriority
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -58,10 +62,10 @@ class Downloader(
|
|||||||
private val sourceManager: SourceManager
|
private val sourceManager: SourceManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
private val chapterCache: ChapterCache by injectLazy()
|
private val chapterCache: ChapterCache by injectLazy()
|
||||||
|
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for persisting downloads across restarts.
|
* Store for persisting downloads across restarts.
|
||||||
*/
|
*/
|
||||||
@@ -164,6 +168,11 @@ class Downloader(
|
|||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if downloader is paused
|
||||||
|
*/
|
||||||
|
fun isPaused() = !isRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*
|
*
|
||||||
@@ -211,7 +220,7 @@ class Downloader(
|
|||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
DownloadService.stop(context)
|
DownloadService.stop(context)
|
||||||
Timber.e(error)
|
logcat(LogPriority.ERROR, error)
|
||||||
notifier.onError(error.message)
|
notifier.onError(error.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -264,7 +273,17 @@ class Downloader(
|
|||||||
|
|
||||||
// Start downloader if needed
|
// Start downloader if needed
|
||||||
if (autoStart && wasEmpty) {
|
if (autoStart && wasEmpty) {
|
||||||
DownloadService.start(this@Downloader.context)
|
val maxDownloadsFromSource = queue
|
||||||
|
.groupBy { it.source }
|
||||||
|
.filterKeys { it !is UnmeteredSource }
|
||||||
|
.maxOf { it.value.size }
|
||||||
|
// TODO: re-enable warning
|
||||||
|
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
|
// withUIContext {
|
||||||
|
// context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
DownloadService.start(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +299,7 @@ class Downloader(
|
|||||||
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
|
||||||
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
|
||||||
download.status = Download.State.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
|
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name, download.manga.title)
|
||||||
return@defer Observable.just(download)
|
return@defer Observable.just(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +320,10 @@ class Downloader(
|
|||||||
Observable.just(download.pages!!)
|
Observable.just(download.pages!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dataSaver = if (preferences.dataSaverDownloader().get()) {
|
||||||
|
DataSaver(download.source, preferences)
|
||||||
|
} else DataSaver.NoOp
|
||||||
|
|
||||||
pageListObservable
|
pageListObservable
|
||||||
.doOnNext { _ ->
|
.doOnNext { _ ->
|
||||||
// Delete all temporary (unfinished) files
|
// Delete all temporary (unfinished) files
|
||||||
@@ -315,7 +338,7 @@ class Downloader(
|
|||||||
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
|
||||||
// Start downloading images, consider we can have downloaded images already
|
// Start downloading images, consider we can have downloaded images already
|
||||||
// Concurrently do 5 pages at a time
|
// Concurrently do 5 pages at a time
|
||||||
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
|
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir, dataSaver) }, 5)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
// Do when page is downloaded.
|
// Do when page is downloaded.
|
||||||
.doOnNext { notifier.onProgressChange(download) }
|
.doOnNext { notifier.onProgressChange(download) }
|
||||||
@@ -326,7 +349,7 @@ class Downloader(
|
|||||||
// If the page list threw, it will resume here
|
// If the page list threw, it will resume here
|
||||||
.onErrorReturn { error ->
|
.onErrorReturn { error ->
|
||||||
download.status = Download.State.ERROR
|
download.status = Download.State.ERROR
|
||||||
notifier.onError(error.message, download.chapter.name)
|
notifier.onError(error.message, download.chapter.name, download.manga.title)
|
||||||
download
|
download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +362,7 @@ class Downloader(
|
|||||||
* @param download the download of the page.
|
* @param download the download of the page.
|
||||||
* @param tmpDir the temporary directory of the download.
|
* @param tmpDir the temporary directory of the download.
|
||||||
*/
|
*/
|
||||||
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
|
private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile, dataSaver: DataSaver): Observable<Page> {
|
||||||
// If the image URL is empty, do nothing
|
// If the image URL is empty, do nothing
|
||||||
if (page.imageUrl == null) {
|
if (page.imageUrl == null) {
|
||||||
return Observable.just(page)
|
return Observable.just(page)
|
||||||
@@ -358,7 +381,7 @@ class Downloader(
|
|||||||
val pageObservable = when {
|
val pageObservable = when {
|
||||||
imageFile != null -> Observable.just(imageFile)
|
imageFile != null -> Observable.just(imageFile)
|
||||||
chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
|
||||||
else -> downloadImage(page, download.source, tmpDir, filename)
|
else -> downloadImage(page, download.source, tmpDir, filename, dataSaver)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageObservable
|
return pageObservable
|
||||||
@@ -386,10 +409,10 @@ class Downloader(
|
|||||||
* @param tmpDir the temporary directory of the download.
|
* @param tmpDir the temporary directory of the download.
|
||||||
* @param filename the filename of the image.
|
* @param filename the filename of the image.
|
||||||
*/
|
*/
|
||||||
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
|
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String, dataSaver: DataSaver): Observable<UniFile> {
|
||||||
page.status = Page.DOWNLOAD_IMAGE
|
page.status = Page.DOWNLOAD_IMAGE
|
||||||
page.progress = 0
|
page.progress = 0
|
||||||
return source.fetchImage(page)
|
return source.fetchImage(page, dataSaver)
|
||||||
.map { response ->
|
.map { response ->
|
||||||
val file = tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.createFile("$filename.tmp")
|
||||||
try {
|
try {
|
||||||
@@ -473,35 +496,7 @@ class Downloader(
|
|||||||
// Only rename the directory if it's downloaded.
|
// Only rename the directory if it's downloaded.
|
||||||
if (download.status == Download.State.DOWNLOADED) {
|
if (download.status == Download.State.DOWNLOADED) {
|
||||||
if (preferences.saveChaptersAsCBZ().get()) {
|
if (preferences.saveChaptersAsCBZ().get()) {
|
||||||
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
archiveChapter(mangaDir, dirname, tmpDir)
|
||||||
val zipOut = ZipOutputStream(BufferedOutputStream(zip.openOutputStream()))
|
|
||||||
val compressionLevel = preferences.saveChaptersAsCBZLevel().get()
|
|
||||||
|
|
||||||
zipOut.setLevel(compressionLevel)
|
|
||||||
|
|
||||||
if (compressionLevel == 0) {
|
|
||||||
zipOut.setMethod(ZipEntry.STORED)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir.listFiles()?.forEach { img ->
|
|
||||||
val input = img.openInputStream()
|
|
||||||
val data = input.readBytes()
|
|
||||||
val entry = ZipEntry(img.name)
|
|
||||||
if (compressionLevel == 0) {
|
|
||||||
val crc = CRC32()
|
|
||||||
val size = img.length()
|
|
||||||
crc.update(data)
|
|
||||||
entry.crc = crc.value
|
|
||||||
entry.compressedSize = size
|
|
||||||
entry.size = size
|
|
||||||
}
|
|
||||||
zipOut.putNextEntry(entry)
|
|
||||||
zipOut.write(data)
|
|
||||||
input.close()
|
|
||||||
}
|
|
||||||
zipOut.close()
|
|
||||||
zip.renameTo("$dirname.cbz")
|
|
||||||
tmpDir.delete()
|
|
||||||
} else {
|
} else {
|
||||||
tmpDir.renameTo(dirname)
|
tmpDir.renameTo(dirname)
|
||||||
}
|
}
|
||||||
@@ -511,6 +506,40 @@ class Downloader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive the chapter pages as a CBZ.
|
||||||
|
*/
|
||||||
|
private fun archiveChapter(
|
||||||
|
mangaDir: UniFile,
|
||||||
|
dirname: String,
|
||||||
|
tmpDir: UniFile,
|
||||||
|
) {
|
||||||
|
val zip = mangaDir.createFile("$dirname.cbz.tmp")
|
||||||
|
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
|
||||||
|
zipOut.setMethod(ZipEntry.STORED)
|
||||||
|
|
||||||
|
tmpDir.listFiles()?.forEach { img ->
|
||||||
|
img.openInputStream().use { input ->
|
||||||
|
val data = input.readBytes()
|
||||||
|
val size = img.length()
|
||||||
|
val entry = ZipEntry(img.name).apply {
|
||||||
|
val crc = CRC32().apply {
|
||||||
|
update(data)
|
||||||
|
}
|
||||||
|
setCrc(crc.value)
|
||||||
|
|
||||||
|
compressedSize = size
|
||||||
|
setSize(size)
|
||||||
|
}
|
||||||
|
zipOut.putNextEntry(entry)
|
||||||
|
zipOut.write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zip.renameTo("$dirname.cbz")
|
||||||
|
tmpDir.delete()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completes a download. This method is called in the main thread.
|
* Completes a download. This method is called in the main thread.
|
||||||
*/
|
*/
|
||||||
@@ -534,8 +563,9 @@ class Downloader(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TMP_DIR_SUFFIX = "_tmp"
|
const val TMP_DIR_SUFFIX = "_tmp"
|
||||||
|
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
|
||||||
// Arbitrary minimum required space to start a download: 50 MB
|
|
||||||
const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arbitrary minimum required space to start a download: 50 MB
|
||||||
|
private const val MIN_DISK_SPACE = 50 * 1024 * 1024
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ class CustomMangaManager(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveMangaInfo(manga: MangaJson) {
|
fun saveMangaInfo(manga: MangaJson) {
|
||||||
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null && manga.status == null) {
|
if (
|
||||||
|
manga.title == null &&
|
||||||
|
manga.author == null &&
|
||||||
|
manga.artist == null &&
|
||||||
|
manga.description == null &&
|
||||||
|
manga.genre == null &&
|
||||||
|
manga.status == null
|
||||||
|
) {
|
||||||
customMangaMap.remove(manga.id!!)
|
customMangaMap.remove(manga.id!!)
|
||||||
} else {
|
} else {
|
||||||
customMangaMap[manga.id!!] = manga.toManga()
|
customMangaMap[manga.id!!] = manga.toManga()
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.preference.CHARGING
|
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
|
||||||
|
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -19,6 +20,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
Worker(context, workerParams) {
|
Worker(context, workerParams) {
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
return if (LibraryUpdateService.start(context)) {
|
return if (LibraryUpdateService.start(context)) {
|
||||||
Result.success()
|
Result.success()
|
||||||
} else {
|
} else {
|
||||||
@@ -33,17 +39,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateRestriction().get()
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
val acRestriction = CHARGING in restrictions
|
|
||||||
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
|
||||||
NetworkType.UNMETERED
|
|
||||||
} else {
|
|
||||||
NetworkType.CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(wifiRestriction)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.setRequiresCharging(acRestriction)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
|
||||||
@@ -61,5 +60,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
|
||||||
|
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
|
||||||
|
return DEVICE_ONLY_ON_WIFI in restrictions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import coil.transform.CircleCropTransformation
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.download.Downloader
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
@@ -52,7 +53,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
val progressNotificationBuilder by lazy {
|
val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
@@ -65,22 +66,25 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that are being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) {
|
fun showProgressNotification(manga: List</* SY --> */SManga/* SY <-- */>, current: Int, total: Int) {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||||
|
.setContentText("($current/$total)")
|
||||||
} else {
|
} else {
|
||||||
manga.title
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(title.chop(40))
|
|
||||||
.setContentText("($current/$total)")
|
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@@ -99,25 +103,12 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_ERROR,
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||||
setStyle(
|
setContentText(context.getString(R.string.action_show_errors))
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
|
||||||
errors.joinToString("\n") {
|
|
||||||
it.chop(NOTIF_TITLE_MAX_LEN)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
|
||||||
|
|
||||||
setContentIntent(errorLogIntent)
|
|
||||||
addAction(
|
|
||||||
R.drawable.ic_folder_24dp,
|
|
||||||
context.getString(R.string.action_show_errors),
|
|
||||||
errorLogIntent
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@@ -204,7 +195,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_black_24dp,
|
R.drawable.ic_glasses_24dp,
|
||||||
context.getString(R.string.action_mark_as_read),
|
context.getString(R.string.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
@@ -223,6 +214,20 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
Notifications.ID_NEW_CHAPTERS
|
Notifications.ID_NEW_CHAPTERS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// Download chapters action
|
||||||
|
// Only add the action when chapters is within threshold
|
||||||
|
if (chapters.size <= Downloader.CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
|
addAction(
|
||||||
|
android.R.drawable.stat_sys_download_done,
|
||||||
|
context.getString(R.string.action_download),
|
||||||
|
NotificationReceiver.downloadChaptersPendingBroadcast(
|
||||||
|
context,
|
||||||
|
manga,
|
||||||
|
chapters,
|
||||||
|
Notifications.ID_NEW_CHAPTERS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
|
||||||
*/
|
|
||||||
object LibraryUpdateRanker {
|
|
||||||
|
|
||||||
val rankingScheme = listOf(
|
|
||||||
(this::lexicographicRanking)(),
|
|
||||||
(this::latestFirstRanking)()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a total ordering over all the [Manga]s.
|
|
||||||
*
|
|
||||||
* Assumption: An active [Manga] mActive is expected to have been last updated after an
|
|
||||||
* inactive [Manga] mInactive.
|
|
||||||
*
|
|
||||||
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
|
|
||||||
* @return a Comparator that ranks manga based on relevance.
|
|
||||||
*/
|
|
||||||
private fun latestFirstRanking(): Comparator<Manga> =
|
|
||||||
Comparator { first: Manga, second: Manga ->
|
|
||||||
compareValues(second.last_update, first.last_update)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides a total ordering over all the [Manga]s.
|
|
||||||
*
|
|
||||||
* Order the manga lexicographically.
|
|
||||||
* @return a Comparator that ranks manga lexicographically based on the title.
|
|
||||||
*/
|
|
||||||
private fun lexicographicRanking(): Comparator<Manga> =
|
|
||||||
Comparator { first: Manga, second: Manga ->
|
|
||||||
compareValues(first.title, second.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,15 +16,17 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadService
|
import eu.kanade.tachiyomi.data.download.DownloadService
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
|
||||||
|
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.GroupLibraryMode
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
@@ -41,6 +43,8 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import exh.log.xLogE
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
import exh.metadata.metadata.base.insertFlatMetadataAsync
|
||||||
@@ -54,7 +58,6 @@ import exh.util.nullIfBlank
|
|||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -64,10 +67,12 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,7 +249,7 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
// Destroy service when completed or in case of an error.
|
// Destroy service when completed or in case of an error.
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
logcat(LogPriority.ERROR, exception)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
updateJob = ioScope.launch(handler) {
|
updateJob = ioScope.launch(handler) {
|
||||||
@@ -278,7 +283,11 @@ class LibraryUpdateService(
|
|||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
libraryManga.filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
// SY -->
|
// SY -->
|
||||||
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
|
} else if (
|
||||||
|
group == LibraryGroup.BY_DEFAULT ||
|
||||||
|
groupLibraryUpdateType == GroupLibraryMode.GLOBAL ||
|
||||||
|
(groupLibraryUpdateType == GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)
|
||||||
|
) {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
||||||
libraryManga.filter { it.category in categoriesToUpdate }
|
libraryManga.filter { it.category in categoriesToUpdate }
|
||||||
@@ -288,9 +297,9 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
|
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
|
||||||
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
|
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
|
||||||
libraryManga.filter { it.category in categoriesToExclude }
|
libraryManga.filter { it.category in categoriesToExclude }.toSet()
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
listToInclude.minus(listToExclude)
|
listToInclude.minus(listToExclude)
|
||||||
@@ -312,13 +321,17 @@ class LibraryUpdateService(
|
|||||||
"not tracked"
|
"not tracked"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
|
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_SOURCE -> {
|
LibraryGroup.BY_SOURCE -> {
|
||||||
val sourceExtra = groupExtra.nullIfBlank()
|
val sourceExtra = groupExtra.nullIfBlank()?.toIntOrNull()
|
||||||
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
|
val source = libraryManga.map { it.source }
|
||||||
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.getOrNull(sourceExtra ?: -1)
|
||||||
|
|
||||||
|
if (source != null) libraryManga.filter { it.source == source } else emptyList()
|
||||||
}
|
}
|
||||||
LibraryGroup.BY_STATUS -> {
|
LibraryGroup.BY_STATUS -> {
|
||||||
val statusExtra = groupExtra?.toIntOrNull() ?: -1
|
val statusExtra = groupExtra?.toIntOrNull() ?: -1
|
||||||
@@ -331,14 +344,30 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
|
||||||
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
if (target == Target.CHAPTERS) {
|
||||||
|
val restrictions = preferences.libraryUpdateMangaRestriction().get()
|
||||||
|
if (MANGA_ONGOING in restrictions) {
|
||||||
|
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
||||||
|
}
|
||||||
|
if (MANGA_FULLY_READ in restrictions) {
|
||||||
|
listToUpdate = listToUpdate.filter { it.unread == 0 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
|
||||||
mangaToUpdate = listToUpdate
|
mangaToUpdate = listToUpdate
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
.sortedBy { it.title }
|
||||||
|
|
||||||
|
// Warn when excessively checking a single source
|
||||||
|
val maxUpdatesFromSource = mangaToUpdate
|
||||||
|
.groupBy { it.source }
|
||||||
|
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
|
||||||
|
.maxOfOrNull { it.value.size } ?: 0
|
||||||
|
// TODO: re-enable warning
|
||||||
|
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
|
||||||
|
// toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -353,32 +382,37 @@ class LibraryUpdateService(
|
|||||||
suspend fun updateChapterList() {
|
suspend fun updateChapterList() {
|
||||||
val semaphore = Semaphore(5)
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
var hasDownloads = false
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
mangaToUpdate.groupBy { it.source }
|
mangaToUpdate.groupBy { it.source }
|
||||||
.filterNot { it.key in LIBRARY_UPDATE_EXCLUDED_SOURCES }
|
.filterNot { it.key in LIBRARY_UPDATE_EXCLUDED_SOURCES }
|
||||||
.values.map { mangaInSource ->
|
.values
|
||||||
|
.map { mangaInSource ->
|
||||||
async {
|
async {
|
||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
mangaInSource
|
mangaInSource.forEach { manga ->
|
||||||
.onEach { manga ->
|
if (updateJob?.isActive != true) {
|
||||||
if (updateJob?.isActive != true) {
|
return@async
|
||||||
return@async
|
}
|
||||||
}
|
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
|
||||||
|
|
||||||
|
withUpdateNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount,
|
||||||
|
manga,
|
||||||
|
) { manga ->
|
||||||
try {
|
try {
|
||||||
val (newChapters, _) = updateManga(manga)
|
val (newChapters, _) = updateManga(manga, loggedServices)
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
downloadChapters(manga, newChapters)
|
downloadChapters(manga, newChapters)
|
||||||
hasDownloads = true
|
hasDownloads.set(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
@@ -388,10 +422,17 @@ class LibraryUpdateService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
val errorMessage = when (e) {
|
||||||
getString(R.string.no_chapters_error)
|
is NoChaptersException -> {
|
||||||
} else {
|
getString(R.string.no_chapters_error)
|
||||||
e.message
|
}
|
||||||
|
is SourceManager.SourceNotInstalledException -> {
|
||||||
|
// failedUpdates will already have the source, don't need to copy it into the message
|
||||||
|
getString(R.string.loader_not_implemented_error)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
}
|
}
|
||||||
@@ -400,21 +441,25 @@ class LibraryUpdateService(
|
|||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(manga, loggedServices)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}
|
||||||
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads) {
|
val newChapterCount = newUpdates.sumOf { it.second.size }
|
||||||
|
preferences.unreadUpdatesCount().set(currentUnreadUpdatesCount + newChapterCount)
|
||||||
|
if (hasDownloads.get()) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
val errorFile = writeErrorFile(failedUpdates)
|
val errorFile = writeErrorFile(failedUpdates)
|
||||||
notifier.showUpdateErrorNotification(
|
notifier.showUpdateErrorNotification(
|
||||||
failedUpdates.map { it.first.title },
|
failedUpdates.map { it.first.title },
|
||||||
@@ -428,10 +473,12 @@ class LibraryUpdateService(
|
|||||||
// may don't like it and they could ban the user.
|
// may don't like it and they could ban the user.
|
||||||
// SY -->
|
// SY -->
|
||||||
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
|
||||||
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
|
db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
|
||||||
|
.filterNot { it.downloadChapters }
|
||||||
|
.mapNotNull { it.mangaId } + manga.id!!
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
// SY <--
|
// SY <--
|
||||||
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
|
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filterNot { it.manga_id in chapterFilter } /* SY <-- */, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -440,40 +487,34 @@ class LibraryUpdateService(
|
|||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
|
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
|
||||||
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
val source = sourceManager.getOrStub(manga.source).getMainSource()
|
||||||
|
|
||||||
// Update manga details metadata in the background
|
// Update manga details metadata
|
||||||
if (preferences.autoUpdateMetadata()) {
|
if (preferences.autoUpdateMetadata()) {
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
||||||
Timber.e(exception)
|
val sManga = updatedManga.toSManga()
|
||||||
|
// Avoid "losing" existing cover
|
||||||
|
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
||||||
|
manga.prepUpdateCover(coverCache, sManga, false)
|
||||||
|
} else {
|
||||||
|
sManga.thumbnail_url = manga.thumbnail_url
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.IO + handler) {
|
|
||||||
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
|
|
||||||
val sManga = updatedManga.toSManga()
|
|
||||||
// Avoid "losing" existing cover
|
|
||||||
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
|
||||||
manga.prepUpdateCover(coverCache, sManga, false)
|
|
||||||
} else {
|
|
||||||
sManga.thumbnail_url = manga.thumbnail_url
|
|
||||||
}
|
|
||||||
|
|
||||||
manga.copyFrom(sManga)
|
manga.copyFrom(sManga)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
if (source.isMdBasedSource() && trackManager.mdList in loggedServices) {
|
||||||
Timber.e(exception)
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
}
|
xLogE("Error adding initial track for ${manga.title}", exception)
|
||||||
ioScope.launch(handler) {
|
}
|
||||||
if (source.isMdBasedSource() && trackManager.mdList.isLogged) {
|
ioScope.launch(handler) {
|
||||||
val tracks = db.getTracks(manga).executeOnIO()
|
val tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
if (tracks.isEmpty() || tracks.none { it.sync_id == TrackManager.MDLIST }) {
|
||||||
var track = trackManager.mdList.createInitialTracker(manga)
|
val track = trackManager.mdList.createInitialTracker(manga)
|
||||||
track = trackManager.mdList.refresh(track)
|
db.insertTrack(trackManager.mdList.refresh(track)).executeAsBlocking()
|
||||||
db.insertTrack(track).executeOnIO()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,29 +531,47 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
var progressCount = 0
|
val semaphore = Semaphore(5)
|
||||||
|
val progressCount = AtomicInteger(0)
|
||||||
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
withUpdateNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
progressCount,
|
||||||
try {
|
manga,
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
) { manga ->
|
||||||
val sManga = networkManga.toSManga()
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
try {
|
||||||
sManga.thumbnail_url?.let {
|
val networkManga =
|
||||||
manga.thumbnail_url = it
|
source.getMangaDetails(manga.toMangaInfo())
|
||||||
db.insertManga(manga).executeAsBlocking()
|
val sManga = networkManga.toSManga()
|
||||||
|
manga.prepUpdateCover(coverCache, sManga, true)
|
||||||
|
sManga.thumbnail_url?.let {
|
||||||
|
manga.thumbnail_url = it
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignore errors and continue
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
}
|
||||||
}
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
coverCache.clearMemoryCache()
|
coverCache.clearMemoryCache()
|
||||||
@@ -532,8 +591,7 @@ class LibraryUpdateService(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify manga that will update.
|
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
|
||||||
|
|
||||||
// Update the tracking details.
|
// Update the tracking details.
|
||||||
updateTrackings(manga, loggedServices)
|
updateTrackings(manga, loggedServices)
|
||||||
@@ -553,12 +611,12 @@ class LibraryUpdateService(
|
|||||||
val updatedTrack = service.refresh(track)
|
val updatedTrack = service.refresh(track)
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
|
||||||
if (service is UnattendedTrackService) {
|
if (service is EnhancedTrackService) {
|
||||||
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ignore errors and continue
|
// Ignore errors and continue
|
||||||
Timber.e(e)
|
logcat(LogPriority.ERROR, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,12 +625,44 @@ class LibraryUpdateService(
|
|||||||
.awaitAll()
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun withUpdateNotification(
|
||||||
|
updatingManga: CopyOnWriteArrayList<LibraryManga>,
|
||||||
|
completed: AtomicInteger,
|
||||||
|
manga: LibraryManga,
|
||||||
|
block: suspend (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
updatingManga,
|
||||||
|
completed.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
|
block(manga)
|
||||||
|
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatingManga.remove(manga)
|
||||||
|
completed.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
updatingManga,
|
||||||
|
completed.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
/**
|
/**
|
||||||
* filter all follows from Mangadex and only add reading or rereading manga to library
|
* filter all follows from Mangadex and only add reading or rereading manga to library
|
||||||
*/
|
*/
|
||||||
private suspend fun syncFollows() {
|
private suspend fun syncFollows() {
|
||||||
val count = AtomicInteger(0)
|
var count = 0
|
||||||
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
|
||||||
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
|
||||||
|
|
||||||
@@ -587,7 +677,8 @@ class LibraryUpdateService(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(networkManga, count.andIncrement, size)
|
count++
|
||||||
|
notifier.showProgressNotification(listOf(networkManga), count, size)
|
||||||
|
|
||||||
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
var dbManga = db.getManga(networkManga.url, mangaDex.id)
|
||||||
.executeOnIO()
|
.executeOnIO()
|
||||||
@@ -616,7 +707,7 @@ class LibraryUpdateService(
|
|||||||
* Method that updates the all mangas which are not tracked as "reading" on mangadex
|
* Method that updates the all mangas which are not tracked as "reading" on mangadex
|
||||||
*/
|
*/
|
||||||
private suspend fun pushFavorites() {
|
private suspend fun pushFavorites() {
|
||||||
val count = AtomicInteger(0)
|
var count = 0
|
||||||
val listManga = db.getFavoriteMangas().executeAsBlocking().filter { it.source in mangaDexSourceIds }
|
val listManga = db.getFavoriteMangas().executeAsBlocking().filter { it.source in mangaDexSourceIds }
|
||||||
|
|
||||||
// filter all follows from Mangadex and only add reading or rereading manga to library
|
// filter all follows from Mangadex and only add reading or rereading manga to library
|
||||||
@@ -626,7 +717,8 @@ class LibraryUpdateService(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, count.andIncrement, listManga.size)
|
count++
|
||||||
|
notifier.showProgressNotification(listOf(manga), count, listManga.size)
|
||||||
|
|
||||||
// Get this manga's trackers from the database
|
// Get this manga's trackers from the database
|
||||||
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
val dbTracks = db.getTracks(manga).executeAsBlocking()
|
||||||
@@ -654,9 +746,20 @@ class LibraryUpdateService(
|
|||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
out.write(getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
// Error file format:
|
||||||
out.write("${manga.title} ($source): $error\n")
|
// ! Error
|
||||||
|
// # Source
|
||||||
|
// - Manga
|
||||||
|
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
||||||
|
out.write("\n! ${error}\n")
|
||||||
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
|
val source = sourceManager.getOrStub(srcId)
|
||||||
|
out.write(" # $source\n")
|
||||||
|
mangas.forEach {
|
||||||
|
out.write(" - ${it.title}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
@@ -667,3 +770,6 @@ class LibraryUpdateService(
|
|||||||
return File("")
|
return File("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ object NotificationHandler {
|
|||||||
*/
|
*/
|
||||||
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
|
||||||
val intent = Intent(context, MainActivity::class.java).apply {
|
val intent = Intent(context, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
action = MainActivity.SHORTCUT_DOWNLOADS
|
action = MainActivity.SHORTCUT_DOWNLOADS
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(context, 0, intent, 0)
|
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -102,6 +102,18 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
markAsRead(urls, mangaId)
|
markAsRead(urls, mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Download manga chapters
|
||||||
|
ACTION_DOWNLOAD_CHAPTER -> {
|
||||||
|
val notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
|
if (notificationId > -1) {
|
||||||
|
dismissNotification(context, notificationId, intent.getIntExtra(EXTRA_GROUP_ID, 0))
|
||||||
|
}
|
||||||
|
val urls = intent.getStringArrayExtra(EXTRA_CHAPTER_URL) ?: return
|
||||||
|
val mangaId = intent.getLongExtra(EXTRA_MANGA_ID, -1)
|
||||||
|
if (mangaId > -1) {
|
||||||
|
downloadChapters(urls, mangaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Share crash dump file
|
// Share crash dump file
|
||||||
ACTION_SHARE_CRASH_LOG ->
|
ACTION_SHARE_CRASH_LOG ->
|
||||||
shareFile(
|
shareFile(
|
||||||
@@ -130,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
val uri = File(path).getUriCompat(context)
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = "image/*"
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,16 +154,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = fileMimeType
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
// Dismiss notification
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||||
context.startActivity(sendIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,7 +204,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelRestore(context: Context, notificationId: Int) {
|
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||||
BackupRestoreService.stop(context)
|
BackupRestoreService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,7 +215,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
||||||
LibraryUpdateService.stop(context)
|
LibraryUpdateService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,6 +247,24 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when user wants to download chapters
|
||||||
|
*
|
||||||
|
* @param chapterUrls URLs of chapter to download
|
||||||
|
* @param mangaId id of manga
|
||||||
|
*/
|
||||||
|
private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) {
|
||||||
|
val db: DatabaseHelper = Injekt.get()
|
||||||
|
|
||||||
|
launchIO {
|
||||||
|
val chapters = chapterUrls.mapNotNull { db.getChapter(it, mangaId).executeAsBlocking() }
|
||||||
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
|
if (chapters.isNotEmpty() && manga != null) {
|
||||||
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@@ -267,6 +281,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||||
|
private const val ACTION_DOWNLOAD_CHAPTER = "$ID.$NAME.ACTION_DOWNLOAD_CHAPTER"
|
||||||
|
|
||||||
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
|
||||||
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
|
||||||
@@ -458,6 +473,28 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [PendingIntent] that downloads chapters
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param manga manga of chapter
|
||||||
|
*/
|
||||||
|
internal fun downloadChaptersPendingBroadcast(
|
||||||
|
context: Context,
|
||||||
|
manga: Manga,
|
||||||
|
chapters: Array<Chapter>,
|
||||||
|
groupId: Int
|
||||||
|
): PendingIntent {
|
||||||
|
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
|
action = ACTION_DOWNLOAD_CHAPTER
|
||||||
|
putExtra(EXTRA_CHAPTER_URL, chapters.map { it.url }.toTypedArray())
|
||||||
|
putExtra(EXTRA_MANGA_ID, manga.id)
|
||||||
|
putExtra(EXTRA_NOTIFICATION_ID, manga.id.hashCode())
|
||||||
|
putExtra(EXTRA_GROUP_ID, groupId)
|
||||||
|
}
|
||||||
|
return PendingIntent.getBroadcast(context, manga.id.hashCode(), newIntent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns [PendingIntent] that starts a service which stops the library update
|
* Returns [PendingIntent] that starts a service which stops the library update
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.notification
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationChannelGroup
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.buildNotificationChannel
|
||||||
|
import eu.kanade.tachiyomi.util.system.buildNotificationChannelGroup
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage the basic information of all the notifications used in the app.
|
* Class to manage the basic information of all the notifications used in the app.
|
||||||
@@ -17,14 +18,15 @@ object Notifications {
|
|||||||
* Common notification channel and ids used anywhere.
|
* Common notification channel and ids used anywhere.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_COMMON = "common_channel"
|
const val CHANNEL_COMMON = "common_channel"
|
||||||
const val ID_UPDATER = 1
|
|
||||||
const val ID_DOWNLOAD_IMAGE = 2
|
const val ID_DOWNLOAD_IMAGE = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
private const val GROUP_LIBRARY = "group_library"
|
||||||
|
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = -101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,12 +47,6 @@ object Notifications {
|
|||||||
const val ID_NEW_CHAPTERS = -301
|
const val ID_NEW_CHAPTERS = -301
|
||||||
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
const val GROUP_NEW_CHAPTERS = "eu.kanade.tachiyomi.NEW_CHAPTERS"
|
||||||
|
|
||||||
/**
|
|
||||||
* Notification channel and ids used by the library updater.
|
|
||||||
*/
|
|
||||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
*/
|
*/
|
||||||
@@ -74,101 +70,112 @@ object Notifications {
|
|||||||
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||||
const val ID_INCOGNITO_MODE = -701
|
const val ID_INCOGNITO_MODE = -701
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel and ids used for app and extension updates.
|
||||||
|
*/
|
||||||
|
private const val GROUP_APK_UPDATES = "group_apk_updates"
|
||||||
|
const val CHANNEL_APP_UPDATE = "app_apk_update_channel"
|
||||||
|
const val ID_APP_UPDATER = 1
|
||||||
|
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
|
||||||
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel",
|
||||||
|
"library_channel",
|
||||||
|
"library_progress_channel",
|
||||||
|
"updates_ext_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
|
* This won't do anything on Android versions that don't support notification channels.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
val notificationService = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
listOf(
|
|
||||||
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
|
||||||
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
|
||||||
).forEach(context.notificationManager::createNotificationChannelGroup)
|
|
||||||
|
|
||||||
listOf(
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_COMMON,
|
|
||||||
context.getString(R.string.channel_common),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_LIBRARY,
|
|
||||||
context.getString(R.string.channel_library),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_ERROR,
|
|
||||||
context.getString(R.string.channel_errors),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_NEW_CHAPTERS,
|
|
||||||
context.getString(R.string.channel_new_chapters),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_UPDATES_TO_EXTS,
|
|
||||||
context.getString(R.string.channel_ext_updates),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
setSound(null, null)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_CRASH_LOGS,
|
|
||||||
context.getString(R.string.channel_crash_logs),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_INCOGNITO_MODE,
|
|
||||||
context.getString(R.string.pref_incognito_mode),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
)
|
|
||||||
).forEach(context.notificationManager::createNotificationChannel)
|
|
||||||
|
|
||||||
// Delete old notification channels
|
// Delete old notification channels
|
||||||
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
||||||
|
|
||||||
|
notificationService.createNotificationChannelGroupsCompat(
|
||||||
|
listOf(
|
||||||
|
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||||
|
setName(context.getString(R.string.label_backup))
|
||||||
|
},
|
||||||
|
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||||
|
setName(context.getString(R.string.download_notifier_downloader_title))
|
||||||
|
},
|
||||||
|
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||||
|
setName(context.getString(R.string.label_library))
|
||||||
|
},
|
||||||
|
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
|
||||||
|
setName(context.getString(R.string.label_recent_updates))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationService.createNotificationChannelsCompat(
|
||||||
|
listOf(
|
||||||
|
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_common))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||||
|
setName(context.getString(R.string.channel_new_chapters))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
setSound(null, null)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_crash_logs))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.pref_incognito_mode))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_DEFAULT) {
|
||||||
|
setGroup(GROUP_APK_UPDATES)
|
||||||
|
setName(context.getString(R.string.channel_app_updates))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_EXTENSIONS_UPDATE, IMPORTANCE_DEFAULT) {
|
||||||
|
setGroup(GROUP_APK_UPDATES)
|
||||||
|
setName(context.getString(R.string.channel_ext_updates))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,141 +5,28 @@ package eu.kanade.tachiyomi.data.preference
|
|||||||
*/
|
*/
|
||||||
object PreferenceKeys {
|
object PreferenceKeys {
|
||||||
|
|
||||||
const val themeMode = "pref_theme_mode_key"
|
|
||||||
|
|
||||||
const val themeLight = "pref_theme_light_key"
|
|
||||||
|
|
||||||
const val themeDark = "pref_theme_dark_key"
|
|
||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
|
||||||
|
|
||||||
const val showSideNavOnBottom = "pref_show_side_nav_on_bottom"
|
|
||||||
|
|
||||||
const val enableTransitionsPager = "pref_enable_transitions_pager_key"
|
|
||||||
|
|
||||||
const val enableTransitionsWebtoon = "pref_enable_transitions_webtoon_key"
|
|
||||||
|
|
||||||
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
|
|
||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
|
||||||
|
|
||||||
const val dualPageSplitPaged = "pref_dual_page_split"
|
|
||||||
|
|
||||||
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
|
|
||||||
|
|
||||||
const val dualPageInvertPaged = "pref_dual_page_invert"
|
|
||||||
|
|
||||||
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
|
|
||||||
|
|
||||||
const val showReadingMode = "pref_show_reading_mode"
|
const val showReadingMode = "pref_show_reading_mode"
|
||||||
|
|
||||||
const val trueColor = "pref_true_color_key"
|
|
||||||
|
|
||||||
const val fullscreen = "fullscreen"
|
|
||||||
|
|
||||||
const val cutoutShort = "cutout_short"
|
|
||||||
|
|
||||||
const val keepScreenOn = "pref_keep_screen_on_key"
|
|
||||||
|
|
||||||
const val customBrightness = "pref_custom_brightness_key"
|
|
||||||
|
|
||||||
const val customBrightnessValue = "custom_brightness_value"
|
|
||||||
|
|
||||||
const val colorFilter = "pref_color_filter_key"
|
|
||||||
|
|
||||||
const val colorFilterValue = "color_filter_value"
|
|
||||||
|
|
||||||
const val colorFilterMode = "color_filter_mode"
|
|
||||||
|
|
||||||
const val grayscale = "pref_grayscale"
|
|
||||||
|
|
||||||
const val defaultReadingMode = "pref_default_reading_mode_key"
|
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||||
|
|
||||||
const val defaultOrientationType = "pref_default_orientation_type_key"
|
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
|
||||||
|
|
||||||
const val zoomStart = "pref_zoom_start_key"
|
|
||||||
|
|
||||||
const val readerTheme = "pref_reader_theme_key"
|
|
||||||
|
|
||||||
const val cropBorders = "crop_borders"
|
|
||||||
|
|
||||||
const val cropBordersWebtoon = "crop_borders_webtoon"
|
|
||||||
|
|
||||||
const val readWithTapping = "reader_tap"
|
|
||||||
|
|
||||||
const val pagerNavInverted = "reader_tapping_inverted"
|
|
||||||
|
|
||||||
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
|
|
||||||
|
|
||||||
const val readWithLongTap = "reader_long_tap"
|
|
||||||
|
|
||||||
const val readWithVolumeKeys = "reader_volume_keys"
|
|
||||||
|
|
||||||
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
|
|
||||||
|
|
||||||
const val navigationModePager = "reader_navigation_mode_pager"
|
|
||||||
|
|
||||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
|
||||||
|
|
||||||
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
|
||||||
|
|
||||||
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
|
||||||
|
|
||||||
const val webtoonSidePadding = "webtoon_side_padding"
|
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
|
||||||
|
|
||||||
const val landscapeColumns = "pref_library_columns_landscape_key"
|
|
||||||
|
|
||||||
const val jumpToChapters = "jump_to_chapters"
|
const val jumpToChapters = "jump_to_chapters"
|
||||||
|
|
||||||
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
|
|
||||||
|
|
||||||
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
|
||||||
|
|
||||||
const val autoAddTrack = "pref_auto_add_track_key"
|
|
||||||
|
|
||||||
const val lastUsedSource = "last_catalogue_source"
|
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
|
||||||
|
|
||||||
const val sourceDisplayMode = "pref_display_mode_catalogue"
|
|
||||||
|
|
||||||
const val enabledLanguages = "source_languages"
|
|
||||||
|
|
||||||
const val backupDirectory = "backup_directory"
|
|
||||||
|
|
||||||
const val downloadsDirectory = "download_directory"
|
|
||||||
|
|
||||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
const val folderPerManga = "create_folder_per_manga"
|
const val folderPerManga = "create_folder_per_manga"
|
||||||
|
|
||||||
const val numberOfBackups = "backup_slots"
|
|
||||||
|
|
||||||
const val backupInterval = "backup_interval"
|
|
||||||
|
|
||||||
const val removeAfterReadSlots = "remove_after_read_slots"
|
const val removeAfterReadSlots = "remove_after_read_slots"
|
||||||
|
|
||||||
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
|
||||||
|
|
||||||
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
const val removeBookmarkedChapters = "pref_remove_bookmarked"
|
||||||
|
|
||||||
const val libraryUpdateInterval = "pref_library_update_interval_key"
|
|
||||||
|
|
||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
|
||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
|
||||||
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
|
||||||
|
|
||||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
|
||||||
|
|
||||||
const val downloadedOnly = "pref_downloaded_only"
|
|
||||||
|
|
||||||
const val filterDownloaded = "pref_filter_library_downloaded"
|
const val filterDownloaded = "pref_filter_library_downloaded"
|
||||||
|
|
||||||
const val filterUnread = "pref_filter_library_unread"
|
const val filterUnread = "pref_filter_library_unread"
|
||||||
@@ -153,40 +40,19 @@ object PreferenceKeys {
|
|||||||
const val filterLewd = "pref_filter_library_lewd"
|
const val filterLewd = "pref_filter_library_lewd"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
const val librarySortingDirection = "library_sorting_ascending"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val migrationSortingMode = "pref_migration_sorting"
|
||||||
|
const val migrationSortingDirection = "pref_migration_direction"
|
||||||
const val showNsfwSource = "show_nsfw_source"
|
|
||||||
const val showNsfwExtension = "show_nsfw_extension"
|
|
||||||
const val labelNsfwExtension = "label_nsfw_extension"
|
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useAuthenticator = "use_biometric_lock"
|
|
||||||
|
|
||||||
const val lockAppAfter = "lock_app_after"
|
|
||||||
|
|
||||||
const val lastAppUnlock = "last_app_unlock"
|
|
||||||
|
|
||||||
const val secureScreen = "secure_screen"
|
|
||||||
|
|
||||||
const val hideNotificationContent = "hide_notification_content"
|
const val hideNotificationContent = "hide_notification_content"
|
||||||
|
|
||||||
const val autoUpdateMetadata = "auto_update_metadata"
|
const val autoUpdateMetadata = "auto_update_metadata"
|
||||||
|
|
||||||
const val autoUpdateTrackers = "auto_update_trackers"
|
const val autoUpdateTrackers = "auto_update_trackers"
|
||||||
|
|
||||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
|
||||||
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
|
||||||
|
|
||||||
const val libraryDisplayMode = "pref_display_mode_library"
|
|
||||||
|
|
||||||
const val lang = "app_language"
|
|
||||||
|
|
||||||
const val dateFormat = "app_date_format"
|
const val dateFormat = "app_date_format"
|
||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
@@ -195,18 +61,6 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val skipFiltered = "skip_filtered"
|
const val skipFiltered = "skip_filtered"
|
||||||
|
|
||||||
const val downloadBadge = "display_download_badge"
|
|
||||||
|
|
||||||
const val unreadBadge = "display_unread_badge"
|
|
||||||
|
|
||||||
const val localBadge = "display_local_badge"
|
|
||||||
|
|
||||||
const val categoryTabs = "display_category_tabs"
|
|
||||||
|
|
||||||
const val categoryNumberOfItems = "display_number_of_items"
|
|
||||||
|
|
||||||
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
|
||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
const val dohProvider = "doh_provider"
|
const val dohProvider = "doh_provider"
|
||||||
@@ -223,7 +77,9 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number"
|
const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number"
|
||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val verboseLogging = "verbose_logging"
|
||||||
|
|
||||||
|
const val autoClearChapterCache = "auto_clear_chapter_cache"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
@@ -231,157 +87,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
fun trackToken(syncId: Int) = "track_token_$syncId"
|
fun trackToken(syncId: Int) = "track_token_$syncId"
|
||||||
|
|
||||||
const val skipPreMigration = "skip_pre_migration"
|
// SY -->
|
||||||
|
|
||||||
const val eh_showSyncIntro = "eh_show_sync_intro"
|
|
||||||
|
|
||||||
const val eh_readOnlySync = "eh_sync_read_only"
|
|
||||||
|
|
||||||
const val eh_lenientSync = "eh_lenient_sync"
|
|
||||||
|
|
||||||
const val eh_useOrigImages = "eh_useOrigImages"
|
|
||||||
|
|
||||||
const val eh_ehSettingsProfile = "eh_ehSettingsProfile"
|
|
||||||
|
|
||||||
const val eh_exhSettingsProfile = "eh_exhSettingsProfile"
|
|
||||||
|
|
||||||
const val eh_settingsKey = "eh_settingsKey"
|
|
||||||
|
|
||||||
const val eh_sessionCookie = "eh_sessionCookie"
|
|
||||||
|
|
||||||
const val eh_hathPerksCookie = "eh_hathPerksCookie"
|
|
||||||
|
|
||||||
const val eh_enableExHentai = "enable_exhentai"
|
|
||||||
|
|
||||||
const val eh_showSettingsUploadWarning = "eh_showSettingsUploadWarning2"
|
|
||||||
|
|
||||||
const val eh_expandFilters = "eh_expand_filters"
|
|
||||||
|
|
||||||
const val eh_readerThreads = "eh_reader_threads"
|
|
||||||
|
|
||||||
const val eh_readerInstantRetry = "eh_reader_instant_retry"
|
|
||||||
|
|
||||||
const val eh_utilAutoscrollInterval = "eh_util_autoscroll_interval"
|
|
||||||
|
|
||||||
const val eh_cacheSize = "eh_cache_size"
|
|
||||||
|
|
||||||
const val eh_preserveReadingPosition = "eh_preserve_reading_position"
|
|
||||||
|
|
||||||
const val eh_autoSolveCaptchas = "eh_autosolve_captchas"
|
|
||||||
|
|
||||||
const val eh_delegateSources = "eh_delegate_sources"
|
|
||||||
|
|
||||||
const val eh_logLevel = "eh_log_level"
|
const val eh_logLevel = "eh_log_level"
|
||||||
|
// SY <--
|
||||||
const val eh_enableSourceBlacklist = "eh_enable_source_blacklist"
|
|
||||||
|
|
||||||
const val eh_autoUpdateFrequency = "eh_auto_update_frequency"
|
|
||||||
|
|
||||||
const val eh_autoUpdateRestrictions = "eh_auto_update_restrictions"
|
|
||||||
|
|
||||||
const val eh_autoUpdateStats = "eh_auto_update_stats"
|
|
||||||
|
|
||||||
const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
|
|
||||||
|
|
||||||
const val eh_preload_size = "eh_preload_size"
|
|
||||||
|
|
||||||
const val eh_tag_filtering_value = "eh_tag_filtering_value"
|
|
||||||
|
|
||||||
const val eh_tag_watching_value = "eh_tag_watching_value"
|
|
||||||
|
|
||||||
const val eh_is_hentai_enabled = "eh_is_hentai_enabled"
|
|
||||||
|
|
||||||
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
|
|
||||||
|
|
||||||
const val eh_watched_list_default_state = "eh_watched_list_default_state"
|
|
||||||
|
|
||||||
const val eh_settings_languages = "eh_settings_languages"
|
|
||||||
|
|
||||||
const val eh_enabled_categories = "eh_enabled_categories"
|
|
||||||
|
|
||||||
const val eh_ehentai_quality = "ehentai_quality"
|
|
||||||
|
|
||||||
const val eh_enable_hah = "eh_enable_hah"
|
|
||||||
|
|
||||||
const val latest_tab_sources = "latest_tab_sources"
|
|
||||||
|
|
||||||
const val latest_tab_position = "latest_tab_position"
|
|
||||||
|
|
||||||
const val sources_tab_categories = "sources_tab_categories"
|
|
||||||
|
|
||||||
const val sources_tab_categories_filter = "sources_tab_categories_filter"
|
|
||||||
|
|
||||||
const val sources_tab_source_categories = "sources_tab_source_categories"
|
|
||||||
|
|
||||||
const val sourcesSort = "sources_sort"
|
|
||||||
|
|
||||||
const val recommendsInOverflow = "recommends_in_overflow"
|
|
||||||
|
|
||||||
const val enhancedEHentaiView = "enhanced_e_hentai_view"
|
|
||||||
|
|
||||||
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
|
|
||||||
|
|
||||||
const val startReadingButton = "start_reading_button"
|
|
||||||
|
|
||||||
const val groupLibraryBy = "group_library_by"
|
|
||||||
|
|
||||||
const val continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
|
|
||||||
|
|
||||||
const val groupLibraryUpdateType = "group_library_update_type"
|
|
||||||
|
|
||||||
const val useNewSourceNavigation = "use_new_source_navigation"
|
|
||||||
|
|
||||||
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
|
||||||
|
|
||||||
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
|
|
||||||
|
|
||||||
const val preferredMangaDexId = "preferred_mangaDex_id"
|
|
||||||
|
|
||||||
const val dataSaver = "data_saver"
|
|
||||||
|
|
||||||
const val ignoreJpeg = "ignore_jpeg"
|
|
||||||
|
|
||||||
const val ignoreGif = "ignore_gif"
|
|
||||||
|
|
||||||
const val dataSaverImageQuality = "data_saver_image_quality"
|
|
||||||
|
|
||||||
const val dataSaverImageFormatJpeg = "data_saver_image_format_jpeg"
|
|
||||||
|
|
||||||
const val dataSaverServer = "data_saver_server"
|
|
||||||
|
|
||||||
const val dataSaverColorBW = "data_saver_color_bw"
|
|
||||||
|
|
||||||
const val saveChaptersAsCBZ = "save_chapter_as_cbz"
|
|
||||||
|
|
||||||
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
|
|
||||||
|
|
||||||
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
|
|
||||||
|
|
||||||
const val authenticatorTimeRanges = "biometric_time_ranges"
|
|
||||||
|
|
||||||
const val sortTagsForLibrary = "sort_tags_for_library"
|
|
||||||
|
|
||||||
const val dontDeleteFromCategories = "dont_delete_from_categories"
|
|
||||||
|
|
||||||
const val extensionRepos = "extension_repos"
|
|
||||||
|
|
||||||
const val cropBordersContinuousVertical = "crop_borders_continues_vertical"
|
|
||||||
|
|
||||||
const val landscapeVerticalSeekbar = "pref_show_vert_seekbar_landscape"
|
|
||||||
|
|
||||||
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
|
|
||||||
|
|
||||||
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
|
|
||||||
|
|
||||||
const val readerBottomButtons = "reader_bottom_buttons"
|
|
||||||
|
|
||||||
const val bottomBarLabels = "pref_show_bottom_bar_labels"
|
|
||||||
|
|
||||||
const val hideUpdatesButton = "pref_hide_updates_button"
|
|
||||||
|
|
||||||
const val hideHistoryButton = "pref_hide_history_button"
|
|
||||||
|
|
||||||
const val pageLayout = "page_layout"
|
|
||||||
|
|
||||||
const val invertDoublePages = "invert_double_pages"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
const val UNMETERED_NETWORK = "wifi"
|
import eu.kanade.tachiyomi.R
|
||||||
const val CHARGING = "ac"
|
|
||||||
|
const val DEVICE_ONLY_ON_WIFI = "wifi"
|
||||||
|
const val DEVICE_CHARGING = "ac"
|
||||||
|
|
||||||
|
const val MANGA_ONGOING = "manga_ongoing"
|
||||||
|
const val MANGA_FULLY_READ = "manga_fully_read"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores the values for the preferences in the application.
|
* This class stores the values for the preferences in the application.
|
||||||
@@ -17,43 +22,54 @@ object PreferenceValues {
|
|||||||
system,
|
system,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class LightThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
strawberrydaiquiri,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
|
||||||
enum class DarkThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
greenapple,
|
|
||||||
midnightdusk,
|
|
||||||
amoled,
|
|
||||||
hotpink,
|
|
||||||
amoledblue,
|
|
||||||
red,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ktlint-enable experimental:enum-entry-name-case */
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
enum class DisplayMode {
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
COMPACT_GRID,
|
DEFAULT(R.string.label_default),
|
||||||
COMFORTABLE_GRID,
|
MONET(R.string.theme_monet),
|
||||||
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
TAKO(R.string.theme_tako),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||||
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
|
||||||
|
// Deprecated
|
||||||
|
DARK_BLUE(null),
|
||||||
|
HOT_PINK(null),
|
||||||
|
BLUE(null),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
NO_TITLE_GRID,
|
PURE_RED(null),
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
LIST,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||||
NONE,
|
NONE,
|
||||||
HORIZONTAL(shouldInvertHorizontal = true),
|
HORIZONTAL(shouldInvertHorizontal = true),
|
||||||
VERTICAL(shouldInvertVertical = true),
|
VERTICAL(shouldInvertVertical = true),
|
||||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
|
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ReaderHideThreshold(val threshold: Int) {
|
||||||
|
HIGHEST(5),
|
||||||
|
HIGH(13),
|
||||||
|
LOW(31),
|
||||||
|
LOWEST(47),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TabletUiMode {
|
||||||
|
AUTOMATIC,
|
||||||
|
ALWAYS,
|
||||||
|
LANDSCAPE,
|
||||||
|
NEVER,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ExtensionInstaller {
|
||||||
|
LEGACY,
|
||||||
|
PACKAGEINSTALLER,
|
||||||
|
SHIZUKU,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.tfcporciuncula.flow.FlowSharedPreferences
|
import com.tfcporciuncula.flow.FlowSharedPreferences
|
||||||
import com.tfcporciuncula.flow.Preference
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||||
|
import eu.kanade.tachiyomi.ui.library.LibraryGroup
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -26,25 +29,6 @@ import java.util.Locale
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
|
|
||||||
block(get())
|
|
||||||
return asFlow()
|
|
||||||
.onEach { block(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
|
||||||
set(get() + item)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
|
||||||
set(get() - item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Preference<Boolean>.toggle(): Boolean {
|
|
||||||
set(!get())
|
|
||||||
return get()
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@@ -66,17 +50,17 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||||
|
|
||||||
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean("pref_hide_bottom_bar_on_scroll", true)
|
||||||
|
|
||||||
fun showSideNavOnBottom() = flowPrefs.getBoolean(Keys.showSideNavOnBottom, false)
|
fun sideNavIconAlignment() = flowPrefs.getInt("pref_side_nav_icon_alignment", 0)
|
||||||
|
|
||||||
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
fun useAuthenticator() = flowPrefs.getBoolean("use_biometric_lock", false)
|
||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
|
||||||
|
|
||||||
fun lastAppUnlock() = flowPrefs.getLong(Keys.lastAppUnlock, 0)
|
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
|
||||||
|
|
||||||
fun secureScreen() = flowPrefs.getBoolean(Keys.secureScreen, false)
|
fun secureScreen() = flowPrefs.getBoolean("secure_screen", false)
|
||||||
|
|
||||||
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
|
||||||
|
|
||||||
@@ -84,111 +68,117 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
fun themeMode() = flowPrefs.getEnum(
|
||||||
|
"pref_theme_mode_key",
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light }
|
||||||
|
)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
fun appTheme() = flowPrefs.getEnum(
|
||||||
|
"pref_app_theme",
|
||||||
|
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT }
|
||||||
|
)
|
||||||
|
|
||||||
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
|
fun themeDarkAmoled() = flowPrefs.getBoolean("pref_theme_dark_amoled_key", false)
|
||||||
|
|
||||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
// SY -->
|
||||||
|
fun pageTransitionsPager() = flowPrefs.getBoolean("pref_enable_transitions_pager_key", true)
|
||||||
|
|
||||||
fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
|
fun pageTransitionsWebtoon() = flowPrefs.getBoolean("pref_enable_transitions_webtoon_key", true)
|
||||||
|
// SY <--
|
||||||
|
|
||||||
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
|
fun doubleTapAnimSpeed() = flowPrefs.getInt("pref_double_tap_anim_speed", 500)
|
||||||
|
|
||||||
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
|
fun showPageNumber() = flowPrefs.getBoolean("pref_show_page_number_key", true)
|
||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun dualPageSplitPaged() = flowPrefs.getBoolean("pref_dual_page_split", false)
|
||||||
|
|
||||||
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
|
fun dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_webtoon", false)
|
||||||
|
|
||||||
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
|
fun dualPageInvertPaged() = flowPrefs.getBoolean("pref_dual_page_invert", false)
|
||||||
|
|
||||||
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
|
fun dualPageInvertWebtoon() = flowPrefs.getBoolean("pref_dual_page_invert_webtoon", false)
|
||||||
|
|
||||||
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
|
|
||||||
|
|
||||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
|
fun trueColor() = flowPrefs.getBoolean("pref_true_color_key", false)
|
||||||
|
|
||||||
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
|
fun fullscreen() = flowPrefs.getBoolean("fullscreen", true)
|
||||||
|
|
||||||
fun cutoutShort() = flowPrefs.getBoolean(Keys.cutoutShort, true)
|
fun cutoutShort() = flowPrefs.getBoolean("cutout_short", true)
|
||||||
|
|
||||||
fun keepScreenOn() = flowPrefs.getBoolean(Keys.keepScreenOn, true)
|
fun keepScreenOn() = flowPrefs.getBoolean("pref_keep_screen_on_key", true)
|
||||||
|
|
||||||
fun customBrightness() = flowPrefs.getBoolean(Keys.customBrightness, false)
|
fun customBrightness() = flowPrefs.getBoolean("pref_custom_brightness_key", false)
|
||||||
|
|
||||||
fun customBrightnessValue() = flowPrefs.getInt(Keys.customBrightnessValue, 0)
|
fun customBrightnessValue() = flowPrefs.getInt("custom_brightness_value", 0)
|
||||||
|
|
||||||
fun colorFilter() = flowPrefs.getBoolean(Keys.colorFilter, false)
|
fun colorFilter() = flowPrefs.getBoolean("pref_color_filter_key", false)
|
||||||
|
|
||||||
fun colorFilterValue() = flowPrefs.getInt(Keys.colorFilterValue, 0)
|
fun colorFilterValue() = flowPrefs.getInt("color_filter_value", 0)
|
||||||
|
|
||||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
fun colorFilterMode() = flowPrefs.getInt("color_filter_mode", 0)
|
||||||
|
|
||||||
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
fun grayscale() = flowPrefs.getBoolean("pref_grayscale", false)
|
||||||
|
|
||||||
|
fun invertedColors() = flowPrefs.getBoolean("pref_inverted_colors", false)
|
||||||
|
|
||||||
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||||
|
|
||||||
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||||
|
|
||||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
fun imageScaleType() = flowPrefs.getInt("pref_image_scale_type_key", 1)
|
||||||
|
|
||||||
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
|
fun zoomStart() = flowPrefs.getInt("pref_zoom_start_key", 1)
|
||||||
|
|
||||||
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
|
fun readerTheme() = flowPrefs.getInt("pref_reader_theme_key", 3)
|
||||||
|
|
||||||
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
fun alwaysShowChapterTransition() = flowPrefs.getBoolean("always_show_chapter_transition", true)
|
||||||
|
|
||||||
fun cropBorders() = flowPrefs.getBoolean(Keys.cropBorders, false)
|
fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
|
||||||
|
|
||||||
fun cropBordersWebtoon() = flowPrefs.getBoolean(Keys.cropBordersWebtoon, false)
|
fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
|
||||||
|
|
||||||
fun webtoonSidePadding() = flowPrefs.getInt(Keys.webtoonSidePadding, 0)
|
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)
|
||||||
|
|
||||||
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
|
fun readWithTapping() = flowPrefs.getBoolean("reader_tap", true)
|
||||||
|
|
||||||
fun pagerNavInverted() = flowPrefs.getEnum(Keys.pagerNavInverted, Values.TappingInvertMode.NONE)
|
fun pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, Values.TappingInvertMode.NONE)
|
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", Values.TappingInvertMode.NONE)
|
||||||
|
|
||||||
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
|
fun readWithLongTap() = flowPrefs.getBoolean("reader_long_tap", true)
|
||||||
|
|
||||||
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
|
fun readWithVolumeKeys() = flowPrefs.getBoolean("reader_volume_keys", false)
|
||||||
|
|
||||||
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
|
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean("reader_volume_keys_inverted", false)
|
||||||
|
|
||||||
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
|
fun navigationModePager() = flowPrefs.getInt("reader_navigation_mode_pager", 0)
|
||||||
|
|
||||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
fun navigationModeWebtoon() = flowPrefs.getInt("reader_navigation_mode_webtoon", 0)
|
||||||
|
|
||||||
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean("reader_navigation_overlay_new_user", true)
|
||||||
|
|
||||||
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean("reader_navigation_overlay_on_start", false)
|
||||||
|
|
||||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
fun readerHideThreshold() = flowPrefs.getEnum("reader_hide_threshold", Values.ReaderHideThreshold.LOW)
|
||||||
|
|
||||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
fun portraitColumns() = flowPrefs.getInt("pref_library_columns_portrait_key", 0)
|
||||||
|
|
||||||
|
fun landscapeColumns() = flowPrefs.getInt("pref_library_columns_landscape_key", 0)
|
||||||
|
|
||||||
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
|
||||||
|
|
||||||
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
|
|
||||||
|
|
||||||
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
|
||||||
|
|
||||||
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
|
fun lastUsedSource() = flowPrefs.getLong("last_catalogue_source", -1)
|
||||||
|
|
||||||
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
|
fun lastUsedCategory() = flowPrefs.getInt("last_used_category", 0)
|
||||||
|
|
||||||
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
|
|
||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.COMPACT_GRID)
|
fun sourceDisplayMode() = flowPrefs.getEnum("pref_display_mode_catalogue", DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet("source_languages", setOf("all", "en", Locale.getDefault().language))
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
@@ -205,22 +195,26 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = flowPrefs.getString("backup_directory", defaultBackupDir.toString())
|
||||||
|
|
||||||
|
fun relativeTime() = flowPrefs.getInt("relative_time", 7)
|
||||||
|
|
||||||
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
|
fun downloadsDirectory() = flowPrefs.getString("download_directory", defaultDownloadsDir.toString())
|
||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
|
||||||
|
|
||||||
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
|
||||||
|
|
||||||
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
|
||||||
|
|
||||||
@@ -228,28 +222,34 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun removeExcludeCategories() = flowPrefs.getStringSet("remove_exclude_categories", emptySet())
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
fun libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
|
||||||
|
|
||||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateDeviceRestriction() = flowPrefs.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))
|
||||||
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_FULLY_READ, MANGA_ONGOING))
|
||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
|
||||||
|
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0)
|
||||||
|
|
||||||
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
|
fun libraryUpdateCategories() = flowPrefs.getStringSet("library_update_categories", emptySet())
|
||||||
|
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet("library_update_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun libraryDisplayMode() = flowPrefs.getEnum("pref_display_mode_library", DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
fun downloadBadge() = flowPrefs.getBoolean("display_download_badge", false)
|
||||||
|
|
||||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
fun localBadge() = flowPrefs.getBoolean("display_local_badge", true)
|
||||||
|
|
||||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
fun downloadedOnly() = flowPrefs.getBoolean("pref_downloaded_only", false)
|
||||||
|
|
||||||
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
|
fun unreadBadge() = flowPrefs.getBoolean("display_unread_badge", true)
|
||||||
|
|
||||||
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
|
fun languageBadge() = flowPrefs.getBoolean("display_language_badge", false)
|
||||||
|
|
||||||
|
fun categoryTabs() = flowPrefs.getBoolean("display_category_tabs", true)
|
||||||
|
|
||||||
|
fun categoryNumberOfItems() = flowPrefs.getBoolean("display_number_of_items", false)
|
||||||
|
|
||||||
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
@@ -263,18 +263,19 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||||
|
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
|
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
||||||
|
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true)
|
||||||
|
|
||||||
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
fun showNsfwSource() = flowPrefs.getBoolean("show_nsfw_source", true)
|
||||||
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
|
|
||||||
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
|
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
|
fun lastAppCheck() = flowPrefs.getLong("last_app_check", 0)
|
||||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||||
|
|
||||||
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
||||||
@@ -283,15 +284,15 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
|
||||||
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
|
||||||
|
|
||||||
fun lang() = prefs.getString(Keys.lang, "")
|
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
|
fun categorizedDisplaySettings() = flowPrefs.getBoolean("categorized_display", false)
|
||||||
|
|
||||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
||||||
@@ -316,7 +317,18 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||||
|
|
||||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
fun incognitoMode() = flowPrefs.getBoolean("incognito_mode", false)
|
||||||
|
|
||||||
|
fun tabletUiMode() = flowPrefs.getEnum("tablet_ui_mode", Values.TabletUiMode.AUTOMATIC)
|
||||||
|
|
||||||
|
fun extensionInstaller() = flowPrefs.getEnum(
|
||||||
|
"extension_installer",
|
||||||
|
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
|
||||||
|
)
|
||||||
|
|
||||||
|
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
|
||||||
|
|
||||||
|
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -328,7 +340,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
||||||
fun defaultMangaOrder() = flowPrefs.getString("default_manga_order", "")
|
fun defaultMangaOrder() = flowPrefs.getString("default_manga_order", "")
|
||||||
@@ -339,58 +350,60 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun useSourceWithMost() = flowPrefs.getBoolean("use_source_with_most", false)
|
fun useSourceWithMost() = flowPrefs.getBoolean("use_source_with_most", false)
|
||||||
|
|
||||||
fun skipPreMigration() = flowPrefs.getBoolean(Keys.skipPreMigration, false)
|
fun skipPreMigration() = flowPrefs.getBoolean("skip_pre_migration", false)
|
||||||
|
|
||||||
fun isHentaiEnabled() = flowPrefs.getBoolean(Keys.eh_is_hentai_enabled, true)
|
fun hideNotFoundMigration() = flowPrefs.getBoolean("hide_not_found_migration", false)
|
||||||
|
|
||||||
fun enableExhentai() = flowPrefs.getBoolean(Keys.eh_enableExHentai, false)
|
fun isHentaiEnabled() = flowPrefs.getBoolean("eh_is_hentai_enabled", true)
|
||||||
|
|
||||||
fun imageQuality() = flowPrefs.getString(Keys.eh_ehentai_quality, "auto")
|
fun enableExhentai() = flowPrefs.getBoolean("enable_exhentai", false)
|
||||||
|
|
||||||
fun useHentaiAtHome() = flowPrefs.getInt(Keys.eh_enable_hah, 0)
|
fun imageQuality() = flowPrefs.getString("ehentai_quality", "auto")
|
||||||
|
|
||||||
|
fun useHentaiAtHome() = flowPrefs.getInt("eh_enable_hah", 0)
|
||||||
|
|
||||||
fun useJapaneseTitle() = flowPrefs.getBoolean("use_jp_title", false)
|
fun useJapaneseTitle() = flowPrefs.getBoolean("use_jp_title", false)
|
||||||
|
|
||||||
fun exhUseOriginalImages() = flowPrefs.getBoolean(Keys.eh_useOrigImages, false)
|
fun exhUseOriginalImages() = flowPrefs.getBoolean("eh_useOrigImages", false)
|
||||||
|
|
||||||
fun ehTagFilterValue() = flowPrefs.getInt(Keys.eh_tag_filtering_value, 0)
|
fun ehTagFilterValue() = flowPrefs.getInt("eh_tag_filtering_value", 0)
|
||||||
|
|
||||||
fun ehTagWatchingValue() = flowPrefs.getInt(Keys.eh_tag_watching_value, 0)
|
fun ehTagWatchingValue() = flowPrefs.getInt("eh_tag_watching_value", 0)
|
||||||
|
|
||||||
// EH Cookies
|
// EH Cookies
|
||||||
fun memberIdVal() = flowPrefs.getString("eh_ipb_member_id", "")
|
fun memberIdVal() = flowPrefs.getString("eh_ipb_member_id", "")
|
||||||
|
|
||||||
fun passHashVal() = flowPrefs.getString("eh_ipb_pass_hash", "")
|
fun passHashVal() = flowPrefs.getString("eh_ipb_pass_hash", "")
|
||||||
fun igneousVal() = flowPrefs.getString("eh_igneous", "")
|
fun igneousVal() = flowPrefs.getString("eh_igneous", "")
|
||||||
fun ehSettingsProfile() = flowPrefs.getInt(Keys.eh_ehSettingsProfile, -1)
|
fun ehSettingsProfile() = flowPrefs.getInt("eh_ehSettingsProfile", -1)
|
||||||
fun exhSettingsProfile() = flowPrefs.getInt(Keys.eh_exhSettingsProfile, -1)
|
fun exhSettingsProfile() = flowPrefs.getInt("eh_exhSettingsProfile", -1)
|
||||||
fun exhSettingsKey() = flowPrefs.getString(Keys.eh_settingsKey, "")
|
fun exhSettingsKey() = flowPrefs.getString("eh_settingsKey", "")
|
||||||
fun exhSessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
|
fun exhSessionCookie() = flowPrefs.getString("eh_sessionCookie", "")
|
||||||
fun exhHathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
|
fun exhHathPerksCookies() = flowPrefs.getString("eh_hathPerksCookie", "")
|
||||||
|
|
||||||
fun exhShowSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
|
fun exhShowSyncIntro() = flowPrefs.getBoolean("eh_show_sync_intro", true)
|
||||||
|
|
||||||
fun exhReadOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
|
fun exhReadOnlySync() = flowPrefs.getBoolean("eh_sync_read_only", false)
|
||||||
|
|
||||||
fun exhLenientSync() = flowPrefs.getBoolean(Keys.eh_lenientSync, false)
|
fun exhLenientSync() = flowPrefs.getBoolean("eh_lenient_sync", false)
|
||||||
|
|
||||||
fun exhShowSettingsUploadWarning() = flowPrefs.getBoolean(Keys.eh_showSettingsUploadWarning, true)
|
fun exhShowSettingsUploadWarning() = flowPrefs.getBoolean("eh_showSettingsUploadWarning2", true)
|
||||||
|
|
||||||
fun expandFilters() = flowPrefs.getBoolean(Keys.eh_expandFilters, false)
|
fun expandFilters() = flowPrefs.getBoolean("eh_expand_filters", false)
|
||||||
|
|
||||||
fun readerThreads() = flowPrefs.getInt(Keys.eh_readerThreads, 2)
|
fun readerThreads() = flowPrefs.getInt("eh_reader_threads", 2)
|
||||||
|
|
||||||
fun readerInstantRetry() = flowPrefs.getBoolean(Keys.eh_readerInstantRetry, true)
|
fun readerInstantRetry() = flowPrefs.getBoolean("eh_reader_instant_retry", true)
|
||||||
|
|
||||||
fun autoscrollInterval() = flowPrefs.getFloat(Keys.eh_utilAutoscrollInterval, 3f)
|
fun autoscrollInterval() = flowPrefs.getFloat("eh_util_autoscroll_interval", 3f)
|
||||||
|
|
||||||
fun cacheSize() = flowPrefs.getString(Keys.eh_cacheSize, "75")
|
fun cacheSize() = flowPrefs.getString("eh_cache_size", "75")
|
||||||
|
|
||||||
fun preserveReadingPosition() = flowPrefs.getBoolean(Keys.eh_preserveReadingPosition, false)
|
fun preserveReadingPosition() = flowPrefs.getBoolean("eh_preserve_reading_position", false)
|
||||||
|
|
||||||
fun autoSolveCaptcha() = flowPrefs.getBoolean(Keys.eh_autoSolveCaptchas, false)
|
fun autoSolveCaptcha() = flowPrefs.getBoolean("eh_autosolve_captchas", false)
|
||||||
|
|
||||||
fun delegateSources() = flowPrefs.getBoolean(Keys.eh_delegateSources, true)
|
fun delegateSources() = flowPrefs.getBoolean("eh_delegate_sources", true)
|
||||||
|
|
||||||
fun ehLastVersionCode() = flowPrefs.getInt("eh_last_version_code", 0)
|
fun ehLastVersionCode() = flowPrefs.getInt("eh_last_version_code", 0)
|
||||||
|
|
||||||
@@ -398,105 +411,109 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun logLevel() = flowPrefs.getInt(Keys.eh_logLevel, 0)
|
fun logLevel() = flowPrefs.getInt(Keys.eh_logLevel, 0)
|
||||||
|
|
||||||
fun enableSourceBlacklist() = flowPrefs.getBoolean(Keys.eh_enableSourceBlacklist, true)
|
fun enableSourceBlacklist() = flowPrefs.getBoolean("eh_enable_source_blacklist", true)
|
||||||
|
|
||||||
fun exhAutoUpdateFrequency() = flowPrefs.getInt(Keys.eh_autoUpdateFrequency, 1)
|
fun exhAutoUpdateFrequency() = flowPrefs.getInt("eh_auto_update_frequency", 1)
|
||||||
|
|
||||||
fun exhAutoUpdateRequirements() = flowPrefs.getStringSet(Keys.eh_autoUpdateRestrictions, emptySet())
|
fun exhAutoUpdateRequirements() = flowPrefs.getStringSet("eh_auto_update_restrictions", emptySet())
|
||||||
|
|
||||||
fun exhAutoUpdateStats() = flowPrefs.getString(Keys.eh_autoUpdateStats, "")
|
fun exhAutoUpdateStats() = flowPrefs.getString("eh_auto_update_stats", "")
|
||||||
|
|
||||||
fun aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
|
fun aggressivePageLoading() = flowPrefs.getBoolean("eh_aggressive_page_loading", false)
|
||||||
|
|
||||||
fun preloadSize() = flowPrefs.getInt(Keys.eh_preload_size, 10)
|
fun preloadSize() = flowPrefs.getInt("eh_preload_size", 10)
|
||||||
|
|
||||||
fun useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
|
fun useAutoWebtoon() = flowPrefs.getBoolean("eh_use_auto_webtoon", true)
|
||||||
|
|
||||||
fun exhWatchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false)
|
fun exhWatchedListDefaultState() = flowPrefs.getBoolean("eh_watched_list_default_state", false)
|
||||||
|
|
||||||
fun exhSettingsLanguages() = flowPrefs.getString(Keys.eh_settings_languages, "false*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false")
|
fun exhSettingsLanguages() = flowPrefs.getString(
|
||||||
|
"eh_settings_languages",
|
||||||
|
"false*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false\nfalse*false*false"
|
||||||
|
)
|
||||||
|
|
||||||
fun exhEnabledCategories() = flowPrefs.getString(Keys.eh_enabled_categories, "false,false,false,false,false,false,false,false,false,false")
|
fun exhEnabledCategories() = flowPrefs.getString(
|
||||||
|
"eh_enabled_categories",
|
||||||
|
"false,false,false,false,false,false,false,false,false,false"
|
||||||
|
)
|
||||||
|
|
||||||
fun latestTabSources() = flowPrefs.getStringSet(Keys.latest_tab_sources, mutableSetOf())
|
fun latestTabSources() = flowPrefs.getStringSet("latest_tab_sources", mutableSetOf())
|
||||||
|
|
||||||
fun latestTabInFront() = flowPrefs.getBoolean(Keys.latest_tab_position, false)
|
fun latestTabInFront() = flowPrefs.getBoolean("latest_tab_position", false)
|
||||||
|
|
||||||
fun sourcesTabCategories() = flowPrefs.getStringSet(Keys.sources_tab_categories, mutableSetOf())
|
fun sourcesTabCategories() = flowPrefs.getStringSet("sources_tab_categories", mutableSetOf())
|
||||||
|
|
||||||
fun sourcesTabCategoriesFilter() = flowPrefs.getBoolean(Keys.sources_tab_categories_filter, false)
|
fun sourcesTabCategoriesFilter() = flowPrefs.getBoolean("sources_tab_categories_filter", false)
|
||||||
|
|
||||||
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
|
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet("sources_tab_source_categories", mutableSetOf())
|
||||||
|
|
||||||
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
|
fun sourceSorting() = flowPrefs.getInt("sources_sort", 0)
|
||||||
|
|
||||||
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
|
fun recommendsInOverflow() = flowPrefs.getBoolean("recommends_in_overflow", false)
|
||||||
|
|
||||||
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
|
fun enhancedEHentaiView() = flowPrefs.getBoolean("enhanced_e_hentai_view", true)
|
||||||
|
|
||||||
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
|
fun webtoonEnableZoomOut() = flowPrefs.getBoolean("webtoon_enable_zoom_out", false)
|
||||||
|
|
||||||
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
|
fun startReadingButton() = flowPrefs.getBoolean("start_reading_button", true)
|
||||||
|
|
||||||
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
|
fun groupLibraryBy() = flowPrefs.getInt("group_library_by", LibraryGroup.BY_DEFAULT)
|
||||||
|
|
||||||
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
|
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean("continuous_vertical_tapping_by_page", false)
|
||||||
|
|
||||||
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
|
fun groupLibraryUpdateType() = flowPrefs.getEnum("group_library_update_type", Values.GroupLibraryMode.GLOBAL)
|
||||||
|
|
||||||
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, true)
|
fun useNewSourceNavigation() = flowPrefs.getBoolean("use_new_source_navigation", true)
|
||||||
|
|
||||||
fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false)
|
fun preferredMangaDexId() = flowPrefs.getString("preferred_mangaDex_id", "0")
|
||||||
|
|
||||||
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet("pref_mangadex_sync_to_library_indexes", emptySet())
|
||||||
|
|
||||||
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
|
fun dataSaver() = flowPrefs.getBoolean("data_saver", false)
|
||||||
|
|
||||||
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
fun dataSaverIgnoreJpeg() = flowPrefs.getBoolean("ignore_jpeg", false)
|
||||||
|
|
||||||
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
fun dataSaverIgnoreGif() = flowPrefs.getBoolean("ignore_gif", true)
|
||||||
|
|
||||||
fun ignoreGif() = flowPrefs.getBoolean(Keys.ignoreGif, true)
|
fun dataSaverImageQuality() = flowPrefs.getInt("data_saver_image_quality", 80)
|
||||||
|
|
||||||
fun dataSaverImageQuality() = flowPrefs.getInt(Keys.dataSaverImageQuality, 80)
|
fun dataSaverImageFormatJpeg() = flowPrefs.getBoolean("data_saver_image_format_jpeg", false)
|
||||||
|
|
||||||
fun dataSaverImageFormatJpeg() = flowPrefs.getBoolean(Keys.dataSaverImageFormatJpeg, false)
|
fun dataSaverServer() = flowPrefs.getString("data_saver_server", "")
|
||||||
|
|
||||||
fun dataSaverServer() = flowPrefs.getString(Keys.dataSaverServer, "")
|
fun dataSaverColorBW() = flowPrefs.getBoolean("data_saver_color_bw", false)
|
||||||
|
|
||||||
fun dataSaverColorBW() = flowPrefs.getBoolean(Keys.dataSaverColorBW, false)
|
fun dataSaverExcludedSources() = flowPrefs.getStringSet("data_saver_excluded", emptySet())
|
||||||
|
|
||||||
fun saveChaptersAsCBZ() = flowPrefs.getBoolean(Keys.saveChaptersAsCBZ, false)
|
fun dataSaverDownloader() = flowPrefs.getBoolean("data_saver_downloader", true)
|
||||||
|
|
||||||
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
|
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean("allow_local_source_hidden_folders", false)
|
||||||
|
|
||||||
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
|
fun authenticatorTimeRanges() = flowPrefs.getStringSet("biometric_time_ranges", mutableSetOf())
|
||||||
|
|
||||||
fun authenticatorTimeRanges() = flowPrefs.getStringSet(Keys.authenticatorTimeRanges, mutableSetOf())
|
fun authenticatorDays() = flowPrefs.getInt("biometric_days", 0x7F)
|
||||||
|
|
||||||
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
|
fun sortTagsForLibrary() = flowPrefs.getStringSet("sort_tags_for_library", mutableSetOf())
|
||||||
|
|
||||||
fun dontDeleteFromCategories() = flowPrefs.getStringSet(Keys.dontDeleteFromCategories, emptySet())
|
fun extensionRepos() = flowPrefs.getStringSet("extension_repos", emptySet())
|
||||||
|
|
||||||
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
|
fun cropBordersContinuousVertical() = flowPrefs.getBoolean("crop_borders_continues_vertical", false)
|
||||||
|
|
||||||
fun cropBordersContinuousVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuousVertical, false)
|
fun forceHorizontalSeekbar() = flowPrefs.getBoolean("pref_force_horz_seekbar", false)
|
||||||
|
|
||||||
fun forceHorizontalSeekbar() = flowPrefs.getBoolean(Keys.forceHorizontalSeekbar, false)
|
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean("pref_show_vert_seekbar_landscape", false)
|
||||||
|
|
||||||
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
|
fun leftVerticalSeekbar() = flowPrefs.getBoolean("pref_left_handed_vertical_seekbar", false)
|
||||||
|
|
||||||
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
|
fun readerBottomButtons() = flowPrefs.getStringSet("reader_bottom_buttons", ReaderBottomButton.BUTTONS_DEFAULTS)
|
||||||
|
|
||||||
fun readerBottomButtons() = flowPrefs.getStringSet(Keys.readerBottomButtons, ReaderBottomButton.BUTTONS_DEFAULTS)
|
fun bottomBarLabels() = flowPrefs.getBoolean("pref_show_bottom_bar_labels", true)
|
||||||
|
|
||||||
fun bottomBarLabels() = flowPrefs.getBoolean(Keys.bottomBarLabels, true)
|
fun showNavUpdates() = flowPrefs.getBoolean("pref_show_updates_button", true)
|
||||||
|
|
||||||
fun hideUpdatesButton() = flowPrefs.getBoolean(Keys.hideUpdatesButton, false)
|
fun showNavHistory() = flowPrefs.getBoolean("pref_show_history_button", true)
|
||||||
|
|
||||||
fun hideHistoryButton() = flowPrefs.getBoolean(Keys.hideHistoryButton, false)
|
fun pageLayout() = flowPrefs.getInt("page_layout", PagerConfig.PageLayout.AUTOMATIC)
|
||||||
|
|
||||||
fun pageLayout() = flowPrefs.getInt(Keys.pageLayout, PagerConfig.PageLayout.SINGLE_PAGE)
|
fun invertDoublePages() = flowPrefs.getBoolean("invert_double_pages", false)
|
||||||
|
|
||||||
fun invertDoublePages() = flowPrefs.getBoolean(Keys.invertDoublePages, false)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||||
|
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
||||||
|
*/
|
||||||
|
interface EnhancedTrackService {
|
||||||
|
/**
|
||||||
|
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||||
|
*/
|
||||||
|
fun accept(source: Source): Boolean {
|
||||||
|
return source::class.qualifiedName in getAcceptedSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified source classes that this track service is compatible with.
|
||||||
|
*/
|
||||||
|
fun getAcceptedSources(): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* match is similar to TrackService.search, but only return zero or one match.
|
||||||
|
*/
|
||||||
|
suspend fun match(manga: Manga): TrackSearch?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the provided source/track/manga triplet is from this TrackService
|
||||||
|
*/
|
||||||
|
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates the given track for the manga to the newSource, if possible
|
||||||
|
*/
|
||||||
|
fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track?
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user