Compare commits
	
		
			732 Commits
		
	
	
		
			ab0893b2d4
			...
			v0.15.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c1064b1ba7 | ||
|  | 3a9f59b7a5 | ||
|  | ccbe240846 | ||
|  | 2c0d96917f | ||
|  | 0316cf47ed | ||
|  | e4c8de1145 | ||
|  | 9c978c608d | ||
|  | b4a88926ed | ||
|  | de593f458f | ||
|  | f3c8b37928 | ||
|  | 91f22c03c0 | ||
|  | 81ee1ce39a | ||
|  | 2ed54eed73 | ||
|  | a9ef4bef8e | ||
|  | c9b988aca6 | ||
|  | b803dbe3af | ||
|  | 35ef07d720 | ||
|  | 4f596d68b9 | ||
|  | 26776f8430 | ||
|  | 471eb36a92 | ||
|  | 8b8b377c29 | ||
|  | d70d2cdff5 | ||
|  | b0704063f2 | ||
|  | dde7254dcf | ||
|  | 4edb9ed398 | ||
|  | 9d764781c3 | ||
|  | f7d991ab54 | ||
|  | 59b3b7d79d | ||
|  | 354bf362c0 | ||
|  | 69304466a7 | ||
|  | 6e4d4739a6 | ||
|  | e8947b3107 | ||
|  | 153641a249 | ||
|  | 60706e8b8b | ||
|  | 19dc859ef2 | ||
|  | 06ff4444c5 | ||
|  | 156cfb0c74 | ||
|  | e49a25b604 | ||
|  | b34a137c07 | ||
|  | 1fb6b3775d | ||
|  | b1c9a204c1 | ||
|  | 8435be1b1f | ||
|  | 7affb9ab63 | ||
|  | 5627ad0801 | ||
|  | 08017a0cd1 | ||
|  | b6603c3425 | ||
|  | f63323feab | ||
|  | eea5f0ba6a | ||
|  | 9f51dfad33 | ||
|  | c31789c112 | ||
|  | 86043fbb31 | ||
|  | 1136ec2ad4 | ||
|  | 7910f6e420 | ||
|  | a087b0bf4b | ||
|  | d0929c1dc5 | ||
|  | 46500dcb32 | ||
|  | f85c34b807 | ||
|  | dfc3a5df42 | ||
|  | 2b78925c68 | ||
|  | eb2fedd669 | ||
|  | b8b5e70151 | ||
|  | 8c00905c99 | ||
|  | 598af5ebd5 | ||
|  | 8adedee973 | ||
|  | 4dc2143160 | ||
|  | aded163e0a | ||
|  | 11a553cbf4 | ||
|  | a0048e9397 | ||
|  | a8a22b5803 | ||
|  | 4122af491c | ||
|  | 0de42b6654 | ||
|  | d7aa1144cf | ||
|  | 855f768e29 | ||
|  | 87fbda0b29 | ||
|  | 99dd9a0750 | ||
|  | c8befdd5ea | ||
|  | 54cf97170d | ||
|  | a5aa89512a | ||
|  | b02813f30a | ||
|  | 51c8430e9c | ||
|  | a846f7da47 | ||
|  | b4e5443e56 | ||
|  | 3fdfd91fff | ||
|  | e4b52c036a | ||
|  | 43098aa61b | ||
|  | e4d8fea138 | ||
|  | 6da22ea8b0 | ||
|  | 5e618e0134 | ||
|  | fd09f64fe6 | ||
|  | ef2241e0cb | ||
|  | bcfe5af135 | ||
|  | a598afec43 | ||
|  | 3c9ec48da3 | ||
|  | 7ce15caded | ||
|  | 7e65e0de0e | ||
|  | 07a70d8c92 | ||
|  | 038574b107 | ||
|  | 24067e1e97 | ||
|  | 68b13c6425 | ||
|  | a9de6a123e | ||
|  | b0251b8177 | ||
|  | c069d75ede | ||
|  | 5886cb7406 | ||
|  | 1713dd4ea0 | ||
|  | f5f7971cb5 | ||
|  | 5271abbd1f | ||
|  | 7d2ded5944 | ||
|  | d2b0319d63 | ||
|  | c15f4c7fd0 | ||
|  | 3aee05bf26 | ||
|  | 9d148a70c8 | ||
|  | 4a64bb250d | ||
|  | e6f5ea172a | ||
|  | 6a0ab3526a | ||
|  | fa29b914cc | ||
|  | 3a79f8fb50 | ||
|  | 321eb38f2d | ||
|  | 46dccf8a2d | ||
|  | 7f02a533fa | ||
|  | 38d329a601 | ||
|  | 1c0f08fc5b | ||
|  | a076deeb5f | ||
|  | a006bc7d93 | ||
|  | 2b215524b6 | ||
|  | f497e605b4 | ||
|  | b09f1fff51 | ||
|  | 92af538b0a | ||
|  | f0b821b122 | ||
|  | 4c5e99142f | ||
|  | b0f39a7d0a | ||
|  | 38b469755f | ||
|  | 3bfda338ef | ||
|  | 6e1da22353 | ||
|  | 60a0303d7f | ||
|  | 6135b4daae | ||
|  | 2fe38192b4 | ||
|  | ef3f4c2e17 | ||
|  | e4e069ccca | ||
|  | 505d2e4274 | ||
|  | 4cdf2f468c | ||
|  | 8c7a2c4262 | ||
|  | 156ce14dd8 | ||
|  | 27dc6443ec | ||
|  | 0a5773211d | ||
|  | b3b0c39163 | ||
|  | bc6b4d4be6 | ||
|  | a14e5a99d9 | ||
|  | d643b947a9 | ||
|  | 2033e82ff0 | ||
|  | c2c872d000 | ||
|  | ae9377f8be | ||
|  | 93a84cad34 | ||
|  | f9ceb92495 | ||
|  | bfdd5c9ec3 | ||
|  | d800528550 | ||
|  | 6ec656b094 | ||
|  | 29c5da1494 | ||
|  | 0a21a9e5f0 | ||
|  | d3aaa3656b | ||
|  | 69a8223d06 | ||
|  | 6959370087 | ||
|  | 1d0937397f | ||
|  | aeeafe8556 | ||
|  | 38c5163ab9 | ||
|  | 4646e4b0d9 | ||
|  | e9f105c52a | ||
|  | 3b225405cb | ||
|  | b96a870c4e | ||
|  | b70d70e82d | ||
|  | f4d1c27cde | ||
|  | 4195b00e48 | ||
|  | 0a37dabf4b | ||
|  | 2d5ac20c46 | ||
|  | 700fd61f34 | ||
|  | a0462fb480 | ||
|  | d9d969406e | ||
|  | f19685787f | ||
|  | 60caba86a5 | ||
|  | 80fa7ebe6c | ||
|  | 9096edfc92 | ||
|  | f30ad78a4c | ||
|  | ce0d99b78f | ||
|  | 3ffb80c6f1 | ||
|  | 4e3c407583 | ||
|  | aaf9fc5d92 | ||
|  | 5043f840a2 | ||
|  | fac3bd1edd | ||
|  | 976cd41a87 | ||
|  | 4aeaee65c3 | ||
|  | f880a0f1a3 | ||
|  | 3a886e39fb | ||
|  | 005d43a3ec | ||
|  | 1d760f4728 | ||
|  | 02e3148efe | ||
|  | e9ada7e5fb | ||
|  | 2ce01a1c86 | ||
|  | 1af8bc95a8 | ||
|  | 0046ed879f | ||
|  | 4f8c6455bc | ||
|  | c80b8c8ce9 | ||
|  | 5435c2fb53 | ||
|  | 88963758b1 | ||
|  | e4d40c7693 | ||
|  | 2f928ac034 | ||
|  | 509d7b20b4 | ||
|  | 35c4d0bf4e | ||
|  | 5e5f2b0f1a | ||
|  | 58135c965c | ||
|  | 5a9b84fe00 | ||
|  | c9a10d9033 | ||
|  | 730c316e89 | ||
|  | 29a07429df | ||
|  | e44616fb3c | ||
|  | cb6ca49607 | ||
|  | ff8b58ee62 | ||
|  | b1350928ee | ||
|  | 52641494d3 | ||
|  | 9bfe4ed829 | ||
|  | d6f0c0837b | ||
|  | 074a1bbca4 | ||
|  | 178cd2b52d | ||
|  | e35f807218 | ||
|  | 63ca5cc66b | ||
|  | 6b66d4a34c | ||
|  | 49580a7630 | ||
|  | 0300c1057b | ||
|  | f5b8d3120d | ||
|  | fd88db37f7 | ||
|  | 39e36f957d | ||
|  | e84c39bf7b | ||
|  | 5641b33bdd | ||
|  | 72681bef83 | ||
|  | 6de16125f7 | ||
|  | 46478546a8 | ||
|  | d4659ffaae | ||
|  | e192806950 | ||
|  | 1170334fde | ||
|  | d368064549 | ||
|  | ada0703d17 | ||
|  | 06d3a753ac | ||
|  | 567c6ea8e7 | ||
|  | bfc0868721 | ||
|  | f5b7a8db1a | ||
|  | bef0a44447 | ||
|  | e9ff202851 | ||
|  | 4a3323f7d0 | ||
|  | 0014a0ca6c | ||
|  | 7e99a9f789 | ||
|  | 03e5c5ca10 | ||
|  | e4ff988f4d | ||
|  | dde2320afc | ||
|  | 988d508025 | ||
|  | 24850450cb | ||
|  | e97d1ac257 | ||
|  | bc44eee5b7 | ||
|  | 7a010e3446 | ||
|  | cb4fbb19ed | ||
|  | ddf0357c5c | ||
|  | 800c01ea42 | ||
|  | 68bfba486e | ||
|  | c09f6cb20c | ||
|  | 959bad0247 | ||
|  | efb8555d76 | ||
|  | a393772083 | ||
|  | b4ade8c15d | ||
|  | 03502f6533 | ||
|  | 42c7669bca | ||
|  | 1916751f4f | ||
|  | e46dd808e7 | ||
|  | 5e92664761 | ||
|  | aa5fd22db0 | ||
|  | 56b1c07f58 | ||
|  | 2ecac08bcc | ||
|  | 95002a6e87 | ||
|  | cea2b42b41 | ||
|  | 8bc5a7d746 | ||
|  | 85723b975b | ||
|  | a2e05cf80b | ||
|  | 87363e47b0 | ||
|  | aefa7a1a4a | ||
|  | 18f90587f2 | ||
|  | f47e9b6e14 | ||
|  | a7e4c1295d | ||
|  | 50097eef9f | ||
|  | 949cdccbae | ||
|  | 3d0dc64de1 | ||
|  | 2e0102d689 | ||
|  | 2557111607 | ||
|  | 46a626d2c5 | ||
|  | fb92b1a0b4 | ||
|  | 8e6ed194a6 | ||
|  | 01175e687c | ||
|  | 00d2d4f969 | ||
|  | 43715f9835 | ||
|  | b99229b033 | ||
|  | 62df1263b1 | ||
|  | a6f0e7f9b9 | ||
|  | f1472d4f8b | ||
|  | 52ad282492 | ||
|  | e7b39f29f2 | ||
|  | c62d3abbc5 | ||
|  | fb3ce226b5 | ||
|  | de5f43713f | ||
|  | 84aba68b96 | ||
|  | 1be0171398 | ||
|  | 82c49a42c3 | ||
|  | 599a4c7b17 | ||
|  | 895dbbdacb | ||
|  | 77026d4eeb | ||
|  | 256b1881f0 | ||
|  | 2a299485cc | ||
|  | a6909a76ed | ||
|  | 1ecba5bb7e | ||
|  | c381b737d3 | ||
|  | 70e7974396 | ||
|  | 2104b60b81 | ||
|  | 09dcb8029c | ||
|  | f2ed26479f | ||
|  | d03d9b344a | ||
|  | 9b5fbda9a2 | ||
|  | 647fbd1f4b | ||
|  | fa6ed901df | ||
|  | 23ac3d18e5 | ||
|  | f18891a07e | ||
|  | c85825f3c7 | ||
|  | 319571edf3 | ||
|  | 1f20d96191 | ||
|  | c2e50305eb | ||
|  | d00e1284d6 | ||
|  | ef4430216d | ||
|  | 643f666178 | ||
|  | 989b968e5b | ||
|  | 92484e26e5 | ||
|  | 429056f2ca | ||
|  | fa2b44eeb3 | ||
|  | 52e742049b | ||
|  | 97a86269e6 | ||
|  | 654d98b5c4 | ||
|  | 0509db1935 | ||
|  | acf879d28c | ||
|  | cefe45e8cc | ||
|  | 03b44fff22 | ||
|  | 71c373926f | ||
|  | 1af11f076f | ||
|  | 41c99c33a6 | ||
|  | 58cce53746 | ||
|  | df6cafd6d1 | ||
|  | cc6c1b5641 | ||
|  | d61adc0259 | ||
|  | 9ed7ee65c3 | ||
|  | 5f94e230f9 | ||
|  | 4d8f44ddae | ||
|  | b20c028566 | ||
|  | 9b883b1a09 | ||
|  | 53402459f2 | ||
|  | bc6a1a1da6 | ||
|  | 690d2fb15b | ||
|  | 7ac188709b | ||
|  | 93c8773e91 | ||
|  | eefe95c2ee | ||
|  | af9825d369 | ||
|  | 59f38cb343 | ||
|  | 810d2d4776 | ||
|  | 027cb10d0f | ||
|  | 24a6eba94e | ||
|  | c1c43bb6fb | ||
|  | 0804550539 | ||
|  | 3156482334 | ||
|  | 052317b38a | ||
|  | 6ff684f638 | ||
|  | 6857c8c1fe | ||
|  | 44d563e689 | ||
|  | 6ce2809450 | ||
|  | 0759036536 | ||
|  | 6f6490b7ac | ||
|  | da12a4b17f | ||
|  | 1302bc84dd | ||
|  | 1c4a8046d0 | ||
|  | 141edac99b | ||
|  | b64ecfb836 | ||
|  | d5f4db5eb7 | ||
|  | 7a3f6e9f63 | ||
|  | b3ecc91be9 | ||
|  | 2a27eacf5e | ||
|  | e747686ad8 | ||
|  | 282af20146 | ||
|  | f25cd9fdbf | ||
|  | 2369547d59 | ||
|  | d86b8cc78c | ||
|  | b4c1e6a44c | ||
|  | 45fc2f2e0e | ||
|  | 9cfcacf45e | ||
|  | 801fd83649 | ||
|  | 16f1dcd922 | ||
|  | 8f36d698cf | ||
|  | a89a36fd4c | ||
|  | 8a7f8c4068 | ||
|  | 9c70e69300 | ||
|  | a0b490b10f | ||
|  | d69dc375a3 | ||
|  | ac6dbbcd89 | ||
|  | 14879c4466 | ||
|  | dce08d4922 | ||
|  | 5195cb8eda | ||
|  | 68de7b516e | ||
|  | 10095205d8 | ||
|  | c4c988f7a4 | ||
|  | 333dfbc642 | ||
|  | e915fd28cb | ||
|  | 46636b537c | ||
|  | cd5545284e | ||
|  | 3dac6366ff | ||
|  | 054a26cf65 | ||
|  | 024c76d480 | ||
|  | 9abdfeac77 | ||
|  | 0f12ae0cf7 | ||
|  | 71182e664a | ||
|  | 7fc6d53cae | ||
|  | 4f2985469c | ||
|  | 9cc24a3be3 | ||
|  | 7ec18861bf | ||
|  | 3d1c02136a | ||
|  | aca34155b9 | ||
|  | d04ffb7ea9 | ||
|  | 5b3e72db54 | ||
|  | f811cc5c87 | ||
|  | e7abe27bb6 | ||
|  | 126d875547 | ||
|  | a2b7228e95 | ||
|  | 10d6b3a6ca | ||
|  | 4c9be5557d | ||
|  | 37e0ac0895 | ||
|  | 8934d251d9 | ||
|  | b5263a6968 | ||
|  | 15bd8e964d | ||
|  | d931027067 | ||
|  | 39ffd3c3bc | ||
|  | 9ff8235de4 | ||
|  | 54075733b7 | ||
|  | 2ea0538825 | ||
|  | 275e20eabd | ||
|  | 7fe742e6ed | ||
|  | 9a6bb69df8 | ||
|  | 4c93ca6914 | ||
|  | 51939ea652 | ||
|  | 256a4a2e7f | ||
|  | c217cab5db | ||
|  | c93b6deaf8 | ||
|  | 55196b86b3 | ||
|  | c5ab79f618 | ||
|  | 59fcfbe9d2 | ||
|  | 6ce70296a6 | ||
|  | fdef687f0c | ||
|  | cdd7f42532 | ||
|  | 65955fd7dc | ||
|  | b785f68154 | ||
|  | 895191814e | ||
|  | b06f253f83 | ||
|  | 0814439886 | ||
|  | a978cb90e8 | ||
|  | 39e0d434ad | ||
|  | 6ada3cbf59 | ||
|  | 4aff768b8e | ||
|  | e08e569135 | ||
|  | 8cac0fca67 | ||
|  | f447b06eff | ||
|  | 78b2045b14 | ||
|  | a427401d66 | ||
|  | 6da6ca710e | ||
|  | f46c42f522 | ||
|  | c4df7b496b | ||
|  | 6c4332f5c2 | ||
|  | 86076d890a | ||
|  | 76a041f504 | ||
|  | c9bdc72d5d | ||
|  | f6a8b23498 | ||
|  | 2a60c828d7 | ||
|  | ea7ff432b2 | ||
|  | 5c2fbec80a | ||
|  | f0109aa796 | ||
|  | 66b92f3eb7 | ||
|  | ec4af65c36 | ||
|  | 852c1a423d | ||
|  | 1904be277d | ||
|  | ebb1022100 | ||
|  | 8d85ec3cd9 | ||
|  | 1d36c3269e | ||
|  | a218f4a48b | ||
|  | b14c3cc49f | ||
|  | cbc4a0f3ab | ||
|  | 70a024d3de | ||
|  | 28d43bbecc | ||
|  | 55a3b2f3a1 | ||
|  | 2545b22ab1 | ||
|  | 77c07d13c0 | ||
|  | 98ac8a69c2 | ||
|  | 0e22af7ba0 | ||
|  | 6951ce34c7 | ||
|  | 24e10d6037 | ||
|  | f1b08bf56e | ||
|  | 3cc3799ebb | ||
|  | aed20a790e | ||
|  | 81b20a23bf | ||
|  | bd27cb74a7 | ||
|  | 07fd3575c5 | ||
|  | 6da504c999 | ||
|  | 23c1827838 | ||
|  | 49833fcc48 | ||
|  | 0e4c0fba5c | ||
|  | bfe67f1cdf | ||
|  | 863e349711 | ||
|  | 5e13df9bd8 | ||
|  | 65abb8fa6c | ||
|  | fe5c0295c3 | ||
|  | 4d15ac2fa3 | ||
|  | 5e4b9fb15b | ||
|  | 8a70dffae8 | ||
|  | 8119eb4b34 | ||
|  | 1bae7ba8f5 | ||
|  | d4f1014df6 | ||
|  | dce685e711 | ||
|  | c4e6668c22 | ||
|  | 9b5608c86d | ||
|  | 2485a00d34 | ||
|  | 8abe2d76b0 | ||
|  | 8e9087226f | ||
|  | 8ccc3c8b0c | ||
|  | 9b3579acf6 | ||
|  | 6f2ff6a77e | ||
|  | f772501159 | ||
|  | cadd389658 | ||
|  | 6f36331818 | ||
|  | d684eb5147 | ||
|  | 349546cf87 | ||
|  | 352593ebc6 | ||
|  | c6b28cbcaf | ||
|  | c5e69d2080 | ||
|  | cd4964d086 | ||
|  | ac3967e997 | ||
|  | 5df18f2558 | ||
|  | 0b054126bc | ||
|  | d4740c57be | ||
|  | 739fc9f95d | ||
|  | 96b1340aec | ||
|  | 57d83e3d1b | ||
|  | 4e2c9dc083 | ||
|  | fa6f60d454 | ||
|  | 71fe2bda97 | ||
|  | f24c5e944e | ||
|  | 603fd84753 | ||
|  | 5fbe1a8614 | ||
|  | 42169783e1 | ||
|  | 90574937df | ||
|  | ec0fe2210d | ||
|  | 450fcccaa6 | ||
|  | 72504ca53f | ||
|  | 6bfddb3ece | ||
|  | b62fcbc2ec | ||
|  | 411dda0e75 | ||
|  | d9d71c8745 | ||
|  | 35239af039 | ||
|  | 840e571917 | ||
|  | 2fd4204db8 | ||
|  | 1fd105ac56 | ||
|  | 6ae90e0a7d | ||
|  | 43dc12a0f3 | ||
|  | 28d05b629f | ||
|  | 30c12fc9de | ||
|  | cdb62f502c | ||
|  | 5cfdbbce7a | ||
|  | e2af1af656 | ||
|  | a96c25c37c | ||
|  | c1abec3b4c | ||
|  | c5a488ca3f | ||
|  | dd4a18a0b2 | ||
|  | 34df0759f3 | ||
|  | b6a3f6ebd2 | ||
|  | ad2819a3bc | ||
|  | 0f2be86d5a | ||
|  | 4342584c32 | ||
|  | 71c10df270 | ||
|  | db90b5d2cb | ||
|  | 5cc38c1369 | ||
|  | e7c48a98df | ||
|  | d03c49db58 | ||
|  | b69af710ad | ||
|  | 4d3b469c48 | ||
|  | 8cc6c0171b | ||
|  | 8be5be4720 | ||
|  | 995a1155e4 | ||
|  | 234c3bb72a | ||
|  | aafe863774 | ||
|  | fb1db914aa | ||
|  | 7aa8abdd98 | ||
|  | 4bd965a795 | ||
|  | df2a4779bf | ||
|  | d2dc063c8e | ||
|  | 45f4c63941 | ||
|  | 5e968e5651 | ||
|  | 3e3c0a1f14 | ||
|  | c48bebe0b2 | ||
|  | 0dd9e9e015 | ||
|  | d2a2e17e91 | ||
|  | 09dbd723e4 | ||
|  | eb965542cc | ||
|  | 615fa05a75 | ||
|  | 87a2ac7887 | ||
|  | 07ce90ab8c | ||
|  | a71ae29c98 | ||
|  | 117214c671 | ||
|  | c54d26d6ba | ||
|  | 5447bd098b | ||
|  | 2b7c0e8e80 | ||
|  | 47966d89f2 | ||
|  | b28a2c3bd4 | ||
|  | 8f51abfc97 | ||
|  | e9bea8ed47 | ||
|  | c2c1b6d2e8 | ||
|  | ded22f1717 | ||
|  | 126da3979c | ||
|  | d892f2f7f4 | ||
|  | f18b32626a | ||
|  | 8c8f2585aa | ||
|  | 1a811d0917 | ||
|  | 9af552c15a | ||
|  | 263cc1d97c | ||
|  | dec4471871 | ||
|  | c8122ebd68 | ||
|  | b766ddea54 | ||
|  | 5cb219d83e | ||
|  | 908128b55d | ||
|  | b240960a8a | ||
|  | b91d8e4c19 | ||
|  | 4853ff7eee | ||
|  | e1582bd73a | ||
|  | ac17335dbe | ||
|  | 7053777997 | ||
|  | 1107b95ebd | ||
|  | 36003fd622 | ||
|  | 84121ff901 | ||
|  | bcc2ec1668 | ||
|  | 08dffda2a1 | ||
|  | 9f4540a4f1 | ||
|  | f19ef9aa01 | ||
|  | f10a06341b | ||
|  | 67ac95f15f | ||
|  | cd291f0a27 | ||
|  | bb6b88a703 | ||
|  | 239b36c31a | ||
|  | dcb6ae44dd | ||
|  | 5ed365cf0d | ||
|  | b20b3d6f5c | ||
|  | 32d02f9329 | ||
|  | 3da7c47bf5 | ||
|  | 029f159aea | ||
|  | 0a300822a4 | ||
|  | 8d48e0289a | ||
|  | 9dbb59f337 | ||
|  | a243aeafba | ||
|  | 5c671de29e | ||
|  | e161e5d30d | ||
|  | 2fd64d0ca7 | ||
|  | d981c75600 | ||
|  | 492adb7035 | ||
|  | 811bca18d9 | ||
|  | 3222e0646d | ||
|  | fa6790856d | ||
|  | 38a15d4158 | ||
|  | 77cd459c51 | ||
|  | fa115ed9a0 | ||
|  | 30a2b572ab | ||
|  | b1ab77188d | ||
|  | e212e4581d | ||
|  | 1b8daa7e09 | ||
|  | 99d126fd3a | ||
|  | 9fee7c6b19 | ||
|  | 1b5a83563f | ||
|  | c8a8eb0a4d | ||
|  | 0a7812bb2c | ||
|  | 061b434687 | ||
|  | f3be1a6d09 | ||
|  | 1e797bfe8a | ||
|  | 97435dbe0f | ||
|  | 3fdbb95b15 | ||
|  | 961a6c0b4f | ||
|  | c42f011a05 | ||
|  | 957c50088d | ||
|  | 3b129176d6 | ||
|  | f36327ecc9 | ||
|  | 5f48bb8e7d | ||
|  | aba8d01818 | ||
|  | 03cb7062f2 | ||
|  | d4b35736c0 | ||
|  | c30aa710f3 | ||
|  | 0adac4217e | ||
|  | 7e102edd0a | ||
|  | adefe87129 | ||
|  | 8a05fc8966 | ||
|  | 8fa798cf63 | ||
|  | 01b20b2acb | ||
|  | a8d72dd6f8 | ||
|  | 84042c61e0 | ||
|  | 407ba6c628 | ||
|  | 16a3c676ef | ||
|  | 9b3a99432b | ||
|  | 036f757086 | ||
|  | a391529b32 | ||
|  | d10b782308 | ||
|  | a58f780452 | ||
|  | 606de03c7c | ||
|  | 2b988c51ad | ||
|  | cb73e55db1 | ||
|  | 8fb4093dc6 | ||
|  | f5c4535cb0 | ||
|  | e4f2bffbc2 | ||
|  | 5c942460f9 | ||
|  | cce1f2c20d | ||
|  | 516a3bd017 | ||
|  | 63d58c7a4f | ||
|  | c49b865ee7 | ||
|  | 097a83e3ba | ||
|  | 02c70d868d | ||
|  | c836f52460 | ||
|  | caa1e1ef09 | ||
|  | 5c63531066 | ||
|  | 15b2bbc6d1 | ||
|  | 3cafbd9141 | ||
|  | dc0405a373 | ||
|  | f84832716a | ||
|  | 200aa4042f | ||
|  | f44c5adcc0 | ||
|  | 3c43bebe64 | 
| @@ -1,8 +0,0 @@ | ||||
| [*.{kt,kts}] | ||||
| max_line_length = 120 | ||||
| indent_size = 4 | ||||
| insert_final_newline = true | ||||
| ij_kotlin_allow_trailing_comma = true | ||||
| ij_kotlin_allow_trailing_comma_on_call_site = true | ||||
| ij_kotlin_name_count_to_use_star_import = 2147483647 | ||||
| ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 | ||||
							
								
								
									
										33
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| 1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** | ||||
| 2. If you are unsure, ask here: [](https://discord.gg/tachiyomi) | ||||
| 3. What is your type of issue? | ||||
|     * [Catalogue request](#catalogue-requests) | ||||
|     * [Bugs](#bugs) | ||||
|     * [Feature requests](#feature-requests) | ||||
|     * [Translations](https://tachiyomi.org/help/contribution/#translation) | ||||
| 4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new) | ||||
|  | ||||
| *** | ||||
|  | ||||
| # Catalogue requests | ||||
|  | ||||
| * Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here | ||||
|  | ||||
| # Bugs | ||||
| * Include version (Setting > About > Version) | ||||
|  * If not latest, try updating, it may have already been solved | ||||
|  * Dev version is equal to the number of commits as seen in the main page | ||||
| * Include steps to reproduce (if not obvious from description) | ||||
| * Include screenshot (if needed) | ||||
| * 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 | ||||
|  | ||||
| DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 | ||||
|  | ||||
| DON'T: https://github.com/inorichi/tachiyomi/issues/75 | ||||
|  | ||||
| # Feature requests | ||||
|  | ||||
| * Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does" | ||||
| * Include screenshot (if needed) | ||||
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,2 @@ | ||||
| github: inorichi | ||||
| ko_fi: inorichi | ||||
							
								
								
									
										26
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| **PLEASE READ THIS** | ||||
|  | ||||
| I acknowledge that: | ||||
|  | ||||
| - I have updated to the latest version of the app (stable is v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **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 | ||||
|  | ||||
| ## Issue/Request | ||||
| ? | ||||
|  | ||||
| ## Other details | ||||
| Additional details and attachments. | ||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| --- | ||||
| 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 v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **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. | ||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,5 +0,0 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: 🖥️ Mihon website | ||||
|     url: https://mihon.app/ | ||||
|     about: Guides, troubleshooting, and answers to common questions | ||||
							
								
								
									
										24
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| --- | ||||
| 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 v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **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) | ||||
							
								
								
									
										104
									
								
								.github/ISSUE_TEMPLATE/report_issue.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,104 +0,0 @@ | ||||
| name: 🐞 Issue report | ||||
| description: Report an issue in Mihon | ||||
| 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 plain text or upload it as an attachment. | ||||
|  | ||||
|   - type: input | ||||
|     id: mihon-version | ||||
|     attributes: | ||||
|       label: Mihon version | ||||
|       description: You can find your Mihon version in **More → About**. | ||||
|       placeholder: | | ||||
|         Example: "0.16.5" | ||||
|     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 or closed issue. | ||||
|           required: true | ||||
|         - label: I have written a short but informative title. | ||||
|           required: true | ||||
|         - label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/). | ||||
|           required: true | ||||
|         - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/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 | ||||
							
								
								
									
										37
									
								
								.github/ISSUE_TEMPLATE/request_feature.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,37 +0,0 @@ | ||||
| name: ⭐ Feature request | ||||
| description: Suggest a feature to improve Mihon | ||||
| labels: [Feature request] | ||||
| body: | ||||
|  | ||||
|   - type: textarea | ||||
|     id: feature-description | ||||
|     attributes: | ||||
|       label: Describe your suggested feature | ||||
|       description: How can Mihon 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 or closed issue. | ||||
|           required: true | ||||
|         - label: I have written a short but informative title. | ||||
|           required: true | ||||
|         - label: I have updated the app to version **[0.16.5](https://github.com/mihonapp/mihon/releases/latest)**. | ||||
|           required: true | ||||
|         - label: I will fill out all of the requested information in this form. | ||||
|           required: true | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/source_issue.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| --- | ||||
| name: "Extension/source/catalogue issue" | ||||
| about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions" | ||||
| title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions" | ||||
| labels: "catalog" | ||||
| --- | ||||
|  | ||||
| DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/assets/logo.png
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 7.5 KiB | 
							
								
								
									
										12
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,12 +0,0 @@ | ||||
| <!-- | ||||
|   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 | | ||||
| | ------- | ------- | | ||||
| |  |  | | ||||
| --> | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/readme-images/app-icon.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.github/readme-images/screens.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										6
									
								
								.github/renovate.json5
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +0,0 @@ | ||||
| { | ||||
|   "$schema": "https://docs.renovatebot.com/renovate-schema.json", | ||||
|   "extends": ["config:base"], | ||||
|   "labels": ["Dependencies"], | ||||
|   "semanticCommits": "disabled" | ||||
| } | ||||
							
								
								
									
										25
									
								
								.github/workflows/PullRequestTester.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,25 @@ | ||||
| name: Pull Request Checker | ||||
|  | ||||
| on: | ||||
|   pull_request: | ||||
|  | ||||
| jobs: | ||||
|   apk: | ||||
|     name: Generate APK | ||||
|     runs-on: ubuntu-18.04 | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: set up JDK 1.8 | ||||
|         uses: actions/setup-java@v1 | ||||
|         with: | ||||
|           java-version: 1.8 | ||||
|       - name: Get NDK  | ||||
|         run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;20.0.5594570" | ||||
|       - name: Build Release APK | ||||
|         run: bash ./gradlew assembleDebug --stacktrace | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         with: | ||||
|           name: TachiyomiSY-${{ github.sha }}.apk | ||||
|           path: app/build/outputs/apk/dev/debug/app-dev-debug.apk | ||||
							
								
								
									
										31
									
								
								.github/workflows/TachiyomiSY-Preview-Builder.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| name: Remote Dispatch Action Initiator | ||||
|   | ||||
| on: | ||||
|   push: | ||||
|   repository_dispatch: | ||||
|   | ||||
| jobs: | ||||
|   ping-pong: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@master | ||||
|         with: | ||||
|           fetch-depth: '0' | ||||
|       - name: TAG - Bump version and push tag | ||||
|         uses: anothrNick/github-tag-action@1.17.2 | ||||
|         if: github.event.action != 'pong' | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|           WITH_V: true | ||||
|           RELEASE_BRANCHES: master | ||||
|       - name: PING - Dispatch initiating repository event | ||||
|         if: github.event.action != 'pong' | ||||
|         run: | | ||||
|           curl -X POST https://api.github.com/repos/jobobby04/TachiyomiSYPreview/dispatches \ | ||||
|           -H 'Accept: application/vnd.github.everest-preview+json' \ | ||||
|           -u ${{ secrets.ACCESS_TOKEN }} \ | ||||
|           --data '{"event_type": "ping", "client_payload": { "repository": "'"$GITHUB_REPOSITORY"'" }}' | ||||
|       - name: ACK - Acknowledge pong from remote repository | ||||
|         if: github.event.action == 'pong' | ||||
|         run: | | ||||
|           echo "PONG received from '${{ github.event.client_payload.repository }}'" | ||||
							
								
								
									
										53
									
								
								.github/workflows/build_pull_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,53 +0,0 @@ | ||||
| name: PR build check | ||||
| on: | ||||
|   pull_request: | ||||
|     paths-ignore: | ||||
|       - '**.md' | ||||
|       - 'i18n/src/commonMain/moko-resources/**/strings.xml' | ||||
|       - 'i18n/src/commonMain/moko-resources/**/plurals.xml' | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build app | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Clone repo | ||||
|         uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||||
|  | ||||
|       - name: Validate Gradle Wrapper | ||||
|         uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Dependency Review | ||||
|         uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: adopt | ||||
|  | ||||
|       - name: Set up gradle | ||||
|         uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Build app and run unit tests | ||||
|         run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: arm64-v8a-${{ github.sha }} | ||||
|           path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk | ||||
|  | ||||
|       - name: Upload mapping | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: mapping-${{ github.sha }} | ||||
|           path: app/build/outputs/mapping/standardRelease | ||||
							
								
								
									
										125
									
								
								.github/workflows/build_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,125 +0,0 @@ | ||||
| name: CI | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|     tags: | ||||
|       - v* | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     name: Build app | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Clone repo | ||||
|         uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 | ||||
|  | ||||
|       - name: Validate Gradle Wrapper | ||||
|         uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Setup Android SDK | ||||
|         run: | | ||||
|           ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "build-tools;29.0.3" | ||||
|  | ||||
|       - name: Set up JDK | ||||
|         uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0 | ||||
|         with: | ||||
|           java-version: 17 | ||||
|           distribution: adopt | ||||
|  | ||||
|       - name: Set up gradle | ||||
|         uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0 | ||||
|  | ||||
|       - name: Build app and run unit tests | ||||
|         run: ./gradlew spotlessCheck assembleStandardRelease testReleaseUnitTest testStandardReleaseUnitTest | ||||
|  | ||||
|       - name: Upload APK | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: arm64-v8a-${{ github.sha }} | ||||
|           path: app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned.apk | ||||
|  | ||||
|       - name: Upload mapping | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: mapping-${{ github.sha }} | ||||
|           path: app/build/outputs/mapping/standardRelease | ||||
|  | ||||
|       # Sign APK and create release for tags | ||||
|  | ||||
|       - name: Get tag name | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         run: | | ||||
|           set -x | ||||
|           echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV | ||||
|  | ||||
|       - name: Sign APK | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1 | ||||
|         with: | ||||
|           releaseDirectory: app/build/outputs/apk/standard/release | ||||
|           signingKeyBase64: ${{ secrets.SIGNING_KEY }} | ||||
|           alias: ${{ secrets.ALIAS }} | ||||
|           keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} | ||||
|           keyPassword: ${{ secrets.KEY_PASSWORD }} | ||||
|  | ||||
|       - name: Clean up build artifacts | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         run: | | ||||
|           set -e | ||||
|  | ||||
|           mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk mihon-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk mihon-x86-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_X86_SHA=$sha" >> $GITHUB_ENV | ||||
|            | ||||
|           cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ env.VERSION_TAG }}.apk | ||||
|           sha=`sha256sum mihon-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'` | ||||
|           echo "APK_X86_64_SHA=$sha" >> $GITHUB_ENV | ||||
|  | ||||
|       - name: Create Release | ||||
|         if: startsWith(github.ref, 'refs/tags/') && github.repository == 'mihonapp/mihon' | ||||
|         uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 | ||||
|         with: | ||||
|           tag_name: ${{ env.VERSION_TAG }} | ||||
|           name: Mihon ${{ env.VERSION_TAG }} | ||||
|           body: | | ||||
|             --- | ||||
|  | ||||
|             ### Checksums | ||||
|  | ||||
|             | Variant | SHA-256 | | ||||
|             | ------- | ------- | | ||||
|             | Universal | ${{ env.APK_UNIVERSAL_SHA }} | ||||
|             | arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }} | ||||
|             | armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }} | ||||
|             | x86 | ${{ env.APK_X86_SHA }} | | ||||
|             | x86_64 | ${{ env.APK_X86_64_SHA }} | | ||||
|              | ||||
|             ## If you are unsure which version to choose then go with mihon-${{ env.VERSION_TAG }}.apk | ||||
|           files: | | ||||
|             mihon-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-arm64-v8a-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-armeabi-v7a-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-x86-${{ env.VERSION_TAG }}.apk | ||||
|             mihon-x86_64-${{ env.VERSION_TAG }}.apk | ||||
|           draft: true | ||||
|           prerelease: false | ||||
|         env: | ||||
|           GITHUB_TOKEN: ${{ secrets.PAT }} | ||||
							
								
								
									
										13
									
								
								.github/workflows/issue_closer.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | ||||
| name: Issue closer | ||||
| on: [issues] | ||||
| jobs: | ||||
|   autoclose: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - name: Autoclose issue | ||||
|       uses: arkon/issue-closer-action@v1.0 | ||||
|       with: | ||||
|         repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         issue-close-message: "@${issue.user.login} this issue was automatically closed because it was not filled in correctly or the acknowledgment section was not removed." | ||||
|         issue-title-pattern: ".*THIS ISSUE IS IN THE WRONG REPO.*" | ||||
|         issue-body-pattern: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*" | ||||
							
								
								
									
										19
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,19 +0,0 @@ | ||||
| name: Lock threads | ||||
|  | ||||
| on: | ||||
|   # Daily | ||||
|   schedule: | ||||
|     - cron: '0 0 * * *' | ||||
|   # Manual trigger | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           issue-inactive-days: '2' | ||||
|           pr-inactive-days: '2' | ||||
							
								
								
									
										15
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -1,12 +1,12 @@ | ||||
| .gradle | ||||
| .kotlin | ||||
| /local.properties | ||||
| /.idea/workspace.xml | ||||
| .DS_Store | ||||
| .idea/* | ||||
| !.idea/icon.png | ||||
| .idea/ | ||||
| *iml | ||||
| *.iml | ||||
| /mainframer | ||||
| /.mainframer | ||||
|  | ||||
| # Built files | ||||
| */build | ||||
| @@ -14,5 +14,10 @@ | ||||
| *.apk | ||||
| app/**/output.json | ||||
|  | ||||
| # Unnecessary file | ||||
| *.swp | ||||
| # Hebrew assets are copied on build | ||||
| app/src/main/res/values-iw/ | ||||
|  | ||||
| TODO.md | ||||
| CHANGELOG.md | ||||
| /captures | ||||
| build.sh | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								.idea/icon.png
									
									
									
										generated
									
									
									
								
							
							
						
						| Before Width: | Height: | Size: 62 KiB | 
							
								
								
									
										81
									
								
								.travis.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,81 @@ | ||||
| dist: trusty | ||||
| language: android | ||||
|  | ||||
| android: | ||||
|   components: | ||||
|     - tools | ||||
|     - platform-tools | ||||
|     - build-tools-29.0.3 | ||||
|     - android-29 | ||||
|     - extra-android-m2repository | ||||
|     - extra-google-m2repository | ||||
|     - extra-android-support | ||||
|     - extra-google-google_play_services | ||||
|  | ||||
|   licenses: | ||||
|     - 'android-sdk-license-.+' | ||||
|     - 'android-sdk-preview-license-.+' | ||||
|  | ||||
| before_install: | ||||
|   - yes | sdkmanager "platforms;android-29" # workaround for accepting the license | ||||
|   - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then | ||||
|     openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d; | ||||
|     tar xf secrets.tar; | ||||
|     mv debug.keystore "$HOME/.android"; | ||||
|     fi | ||||
|   - mkdir "$ANDROID_HOME/licenses" || true | ||||
|   - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license" | ||||
|   - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" | ||||
|  | ||||
| install: | ||||
|   - echo y | sdkmanager "ndk-bundle" | ||||
|  | ||||
| before_script: | ||||
|   - export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle | ||||
|  | ||||
| script: ".travis/build.sh" | ||||
|  | ||||
| before_cache: | ||||
|   - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock | ||||
|   - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ | ||||
|  | ||||
| cache: | ||||
|   directories: | ||||
|     - "$HOME/.gradle/caches/" | ||||
|     - "$HOME/.gradle/wrapper/" | ||||
|     - "$HOME/.android/build-cache" | ||||
|  | ||||
| deploy: | ||||
|   - provider: releases | ||||
|     api_key: | ||||
|       secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA= | ||||
|     file: tachiyomi-v*.apk | ||||
|     file_glob: true | ||||
|     skip_cleanup: true | ||||
|     on: | ||||
|       tags: true | ||||
|       repo: inorichi/tachiyomi | ||||
|   - provider: script | ||||
|     script: ".travis/deploy.sh" | ||||
|     skip_cleanup: true | ||||
|     on: | ||||
|       branch: master | ||||
|       condition: "-z $TRAVIS_TAG" | ||||
|       repo: inorichi/tachiyomi | ||||
|   - provider: script | ||||
|     script: ".travis/deploy.sh" | ||||
|     skip_cleanup: true | ||||
|     on: | ||||
|       branch: dev | ||||
|       condition: "-z $TRAVIS_TAG" | ||||
|       repo: inorichi/tachiyomi | ||||
|  | ||||
| env: | ||||
|   global: | ||||
|     - secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU= | ||||
|     - secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI= | ||||
|     - secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E= | ||||
|     - secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c= | ||||
|     - secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI= | ||||
|     - secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY= | ||||
|     - secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04= | ||||
							
								
								
									
										134
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,134 +0,0 @@ | ||||
| # Changelog | ||||
|  | ||||
| All notable changes to this project will be documented in this file. | ||||
|  | ||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
|  | ||||
| ## [Unreleased] | ||||
| ### Added | ||||
| - Option to disable reader zoom out ([@Splintorien](https://github.com/Splintorien)) ([#302](https://github.com/mihonapp/mihon/pull/302)) | ||||
| - Source name and tracker urls to app generated `ComicInfo.xml` file ([@Shamicen](https://github.com/Shamicen)) ([#459](https://github.com/mihonapp/mihon/pull/459)) | ||||
| - Option to migrate in Duplicate entry dialog ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) | ||||
| - Upcoming screen to visualize expected update dates ([@sirlag](https://github.com/sirlag)) ([#420](https://github.com/mihonapp/mihon/pull/420)) | ||||
| - Crash screen error message to the top of the crash log generated from that screen ([@FooIbar](https://github.com/FooIbar)) ([#742](https://github.com/mihonapp/mihon/pull/742)) | ||||
| - Support for 7Zip and RAR5 archives ([@FooIbar](https://github.com/FooIbar), [@null2264](https://github.com/null2264)) ([#949](https://github.com/mihonapp/mihon/pull/949), [#967](https://github.com/mihonapp/mihon/pull/967)) | ||||
| - Extra configuration options to e-ink page flashes ([@sirlag](https://github.com/sirlag)) ([#625](https://github.com/mihonapp/mihon/pull/625)) | ||||
| - 8-bit+ AVIF image support ([@WerctFourth](https://github.com/WerctFourth)) ([#971](https://github.com/mihonapp/mihon/pull/971)) | ||||
| - Smart update dialog message when no predicted released date exists ([@Animeboynz](https://github.com/Animeboynz)) ([#977](https://github.com/mihonapp/mihon/pull/977)) | ||||
| - Save global search "Has result" choice ([@AntsyLich](https://github.com/AntsyLich)) ([`5a61ca5`](https://github.com/mihonapp/mihon/commit/5a61ca5535fe0d9e8e7bcb9e665ba2f9cb0cf649)) | ||||
| - Option to copy reader panel to clipboard ([@Animeboynz](https://github.com/Animeboynz)) ([#1003](https://github.com/mihonapp/mihon/pull/1003)) | ||||
| - Copy Tracker URL option to tracker sheet ([@mm12](https://github.com/mm12)) ([#1101](https://github.com/mihonapp/mihon/pull/1101)) | ||||
| - A button to exclude all scanlators in exclude scanlators dialog ([@AntsyLich](https://github.com/AntsyLich)) ([`84b2164`](https://github.com/mihonapp/mihon/commit/84b2164787a795f3fd757c325cbfb6ef660ac3a3)) | ||||
| - Open in browser option to reader menu ([@mm12](https://github.com/mm12)) ([#1110](https://github.com/mihonapp/mihon/pull/1110)) | ||||
| - Option to skip downloading duplicate read chapters ([@shabnix](https://github.com/shabnix)) ([#1125](https://github.com/mihonapp/mihon/pull/1125)) | ||||
|  | ||||
| ### Changed | ||||
| - Read archive files from memory instead of extracting files to internal storage ([@FooIbar](https://github.com/FooIbar)) ([#326](https://github.com/mihonapp/mihon/pull/326)) | ||||
| - Try to get resource from Extension before checking in the app ([@beer-psi](https://github.com/beer-psi)) ([#433](https://github.com/mihonapp/mihon/pull/433)) | ||||
| - Default user agent ([@AntsyLich](https://github.com/AntsyLich)) ([`8160b47`](https://github.com/mihonapp/mihon/commit/8160b47ff5fbbd9b32caeb462b5be881fabd3449)) | ||||
| - Wait for sources to be initialized before performing source related tasks ([@jobobby04](https://github.com/jobobby04)) ([`a08e03f`](https://github.com/mihonapp/mihon/commit/a08e03f5cbf3f4e6be1de35f97ef8ebb26a1210e)) | ||||
| - Duplicate entry dialog UI ([@sirlag](https://github.com/sirlag)) ([#492](https://github.com/mihonapp/mihon/pull/492)) | ||||
| - Extension trust system ([@AntsyLich](https://github.com/AntsyLich), [@Animeboynz](https://github.com/Animeboynz) ([#570](https://github.com/mihonapp/mihon/pull/570), [#1057](https://github.com/mihonapp/mihon/pull/1057)) | ||||
| - Make category backup/restore not dependant on library backup ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) | ||||
| - Kitsu domain to `kitsu.app` ([@MajorTanya](https://github.com/MajorTanya)) ([#1106](https://github.com/mihonapp/mihon/pull/1106)) | ||||
| - Respect privacy settings in extension update notification ([@Animeboynz](https://github.com/Animeboynz)) ([#1156](https://github.com/mihonapp/mihon/pull/1156)) | ||||
|  | ||||
| ### Improvement | ||||
| - Long strip reader performance ([@FooIbar](https://github.com/FooIbar), [@wwww-wwww](https://github.com/wwww-wwww)) ([#687](https://github.com/mihonapp/mihon/pull/687)) | ||||
| - Performance when looking up specific files ([@raxod502](https://github.com/raxod502)) ([#728](https://github.com/mihonapp/mihon/pull/728)) | ||||
| - Chapter number parsing ([@Naputt1](https://github.com/Naputt1)) ([`6a80305`](https://github.com/mihonapp/mihon/commit/6a80305d6c572da6c08c0c69f5c25ff26ecf7383)) | ||||
| - Error message on restoring if backup decoding fails ([@vetleledaal](https://github.com/vetleledaal)) ([#1056](https://github.com/mihonapp/mihon/pull/1056)) | ||||
|  | ||||
| ### Fixed | ||||
| - Creating `ComicInfo.xml` file for local source ([@FooIbar](https://github.com/FooIbar)) ([#325](https://github.com/mihonapp/mihon/pull/325)) | ||||
| - Chapter download indicator ([@ivaniskandar](https://github.com/ivaniskandar)) ([`d8b9a9f`](https://github.com/mihonapp/mihon/commit/d8b9a9f593911569ff2bceb49b4f020978d0d2e1)) | ||||
| - Issues with shizuku in a multi user setup ([@Redjard](https://github.com/Redjard)) ([#494](https://github.com/mihonapp/mihon/pull/494)) | ||||
| - Occasional black bar when scrolling in long strip reader ([@FooIbar](https://github.com/FooIbar)) ([#563](https://github.com/mihonapp/mihon/pull/563)) | ||||
| - Extension being marked as not installed instead of untrusted after updating with private installer ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - Extension update counter not updating due to extension being marked as untrusted ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - `Key "extension-XXX-YYY" was already used` crash ([@AntsyLich](https://github.com/AntsyLich)) ([`2114514`](https://github.com/mihonapp/mihon/commit/21145144cdf550aa775047603e06e261951ebc42)) | ||||
| - Navigation layout tap zones shifting after zooming out in webtoon readers ([@FooIbar](https://github.com/FooIbar)) ([#767](https://github.com/mihonapp/mihon/pull/767)) | ||||
| - Some extension not loading due to missing classes ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#783](https://github.com/mihonapp/mihon/pull/783)) | ||||
| - Theme colors in accordance to upstream changes ([@CrepeTF](https://github.com/CrepeTF), [@AntsyLich](https://github.com/AntsyLich)) ([#766](https://github.com/mihonapp/mihon/pull/766), [#963](https://github.com/mihonapp/mihon/pull/963), [#976](https://github.com/mihonapp/mihon/pull/976)) | ||||
| - Crash when requesting folder access on non-conforming devices ([@mainrs](https://github.com/mainrs)) ([#726](https://github.com/mihonapp/mihon/pull/726)) | ||||
| - Bugged color for Date/Scanlator in chapter list for read chapters ([@ivaniskandar](https://github.com/ivaniskandar)) ([`15d9992`](https://github.com/mihonapp/mihon/commit/15d999229fcce865001d5fa77d0163e6e80e38db)) | ||||
| - Categories having same `order` after restoring backup ([@Cologler](https://github.com/Cologler)) ([`119bcbf`](https://github.com/mihonapp/mihon/commit/119bcbf8ed2415664922ea77fadf0da1165d1732)) | ||||
| - Filter by "Tracking" temporarily stuck after signing out of tracker ([@AntsyLich](https://github.com/AntsyLich)) ([#987](https://github.com/mihonapp/mihon/pull/987)) | ||||
| - JXL image downloading and loading ([@FooIbar](https://github.com/FooIbar)) ([#993](https://github.com/mihonapp/mihon/pull/993)) | ||||
| - Crash when using `%` in category name ([@Animeboynz](https://github.com/Animeboynz), [@FooIbar](https://github.com/FooIbar)) ([#1030](https://github.com/mihonapp/mihon/pull/1030)) | ||||
| - Library is backed up while being disabled ([@AntsyLich](https://github.com/AntsyLich)) ([`56fb4f6`](https://github.com/mihonapp/mihon/commit/56fb4f62a152e87a71892aa68c78cac51a2c8596)) | ||||
| - Crash on list with 0 item but only sticky header ([@cuong-tran](https://github.com/cuong-tran)) ([#1083](https://github.com/mihonapp/mihon/pull/1083)) | ||||
| - Crash when trying to clear cookies of some source ([@FooIbar](https://github.com/FooIbar)) ([#1084](https://github.com/mihonapp/mihon/pull/1084)) | ||||
| - MAL search results not showing start dates ([@MajorTanya](https://github.com/MajorTanya)) ([#1098](https://github.com/mihonapp/mihon/pull/1098)) | ||||
| - Android SDK 35 API collision ([@AntsyLich](https://github.com/AntsyLich)) ([`fdb9617`](https://github.com/mihonapp/mihon/commit/fdb96179c6373eb0a8e7d6daea671a315d5ce5f0)) | ||||
|  | ||||
| ## [v0.16.5] - 2024-04-09 | ||||
| ### Added | ||||
| - Setting to install custom color profiles to get true colors ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ### Changed | ||||
| - Permanently enable 32-bit color mode ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix wrong dates in Updates and History tab due to time zone issues ([@sirlag](https://github.com/sirlag)) ([#402](https://github.com/mihonapp/mihon/pull/402), [#415](https://github.com/mihonapp/mihon/pull/415)) | ||||
| - Fix app infinitely retries tracker update instead of failing after 3 tries ([@MajorTanya](https://github.com/MajorTanya)) ([#411](https://github.com/mihonapp/mihon/pull/411)) | ||||
| - Fix crash on Pixel devices ([`ab06720`](https://github.com/mihonapp/mihon/commit/ab067209661eceefc04c65f6bdbfcaa8a1264651)) | ||||
| - Fix crash when opening some heif/heic images ([@az4521](https://github.com/az4521)) ([#466](https://github.com/mihonapp/mihon/pull/466)) | ||||
| - Fix crash in track date selection dialog ([@ivaniskandar](https://github.com/ivaniskandar)) ([`c348fac`](https://github.com/mihonapp/mihon/commit/c348fac78fac479fb123bd617c01c78b9ca851d5)) | ||||
| - Fix dates for saved images on Samsung devices ([@MajorTanya](https://github.com/MajorTanya)) ([#552](https://github.com/mihonapp/mihon/pull/552)) | ||||
| - Fix colors getting distorted when opening CMYK jpeg images ([@wwww-wwww](https://github.com/wwww-wwww)) ([#523](https://github.com/mihonapp/mihon/pull/523)) | ||||
|  | ||||
| ## [v0.16.4] - 2024-02-26 | ||||
| ### Fixed | ||||
| - Circumvent MAL block ([@AntsyLich](https://github.com/AntsyLich)) ([`085ad8d`](https://github.com/mihonapp/mihon/commit/085ad8d44637c375a8ed24aba3a6f75f5b0cc9ee)) | ||||
|  | ||||
| ## [v0.16.3] - 2024-01-30 | ||||
| ### Added | ||||
| - Copy extension debug info when clicking logo or name in the extension details screen ([@MajorTanya](https://github.com/MajorTanya)) ([#271](https://github.com/mihonapp/mihon/pull/271)) | ||||
|  | ||||
| ### Changed | ||||
| - Rename extension update error file to `mihon_update_errors.txt` ([@mjishnu](https://github.com/mjishnu)) ([#253](https://github.com/mihonapp/mihon/pull/253)) | ||||
| - Hide display cutoff setting in reader settings sheet if fullscreen is off ([@Riztard](https://github.com/Riztard)) ([#241](https://github.com/mihonapp/mihon/pull/241)) | ||||
|  | ||||
| ### Fixed | ||||
| - Fix bottom sheet display issues on non-Tablet UI ([@theolm](https://github.com/theolm)) ([#182](https://github.com/mihonapp/mihon/pull/182)) | ||||
| - Fix crash when switching screen while a list is scrolling ([@theolm](https://github.com/theolm)) ([#272](https://github.com/mihonapp/mihon/pull/272)) | ||||
| - Fix newly installed extensions not being recognized by Mihon ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#275](https://github.com/mihonapp/mihon/pull/275)) | ||||
| - Fix error handling when refreshing MAL OAuth token ([@AntsyLich](https://github.com/AntsyLich)) ([`0f4de03`](https://github.com/mihonapp/mihon/commit/0f4de03d7a77b52490dc9a95e96a308b93b26e4f)) | ||||
|  | ||||
| ## [v0.16.2] - 2024-01-28 | ||||
| ### Added | ||||
| - Scanlator filter is now part of Backup ([@jobobby04](https://github.com/jobobby04)) ([#166](https://github.com/mihonapp/mihon/pull/166)) | ||||
|  | ||||
| ### Changed | ||||
| - Rename crash log filename to `mihon_crash_logs.txt` ([@MajorTanya](https://github.com/MajorTanya)) ([#234](https://github.com/mihonapp/mihon/pull/234)) | ||||
|  | ||||
| ### Fixed | ||||
| - "Flash screen on page change" Making the screen goes blank ([@AntsyLich](https://github.com/AntsyLich)) ([`38d6ab8`](https://github.com/mihonapp/mihon/commit/38d6ab80ce868707829dbc81de4170afe3c2f2a5)) | ||||
| - App icon scaling ([@AntsyLich](https://github.com/AntsyLich)) ([`26815c7`](https://github.com/mihonapp/mihon/commit/26815c7356111394665467c1e81255ac9ee33c1a)) | ||||
| - Updating extension not reflecting correctly ([@AntsyLich](https://github.com/AntsyLich)) ([`cb06898`](https://github.com/mihonapp/mihon/commit/cb068984303f811692531bf6f14902ae118d8ac7)) | ||||
| - Inconsistent button height with some languages in "Data and storage" ([@theolm](https://github.com/theolm)) ([#202](https://github.com/mihonapp/mihon/pull/202)) | ||||
| - Fix chapter not being marked as read in some cases with Enhanced Trackers ([@Secozzi](https://github.com/Secozzi)) ([#219](https://github.com/mihonapp/mihon/pull/219))  | ||||
| - And various tracker related fixes ([@AntsyLich](https://github.com/AntsyLich), [@kitsumed](https://github.com/kitsumed), [@Secozzi](https://github.com/Secozzi)) ([`a024218`](https://github.com/mihonapp/mihon/commit/a024218410953a389b8af4880fa7ae6cc30124a2), [`e3f33e2`](https://github.com/mihonapp/mihon/commit/e3f33e24f5e928ac8a85d1f500fd42d4715fc6b5), [`32188f9`](https://github.com/mihonapp/mihon/commit/32188f9f65009a18250674ef1bd6e57d351c1fba)) | ||||
|  | ||||
| ## [v0.16.1] - 2024-01-18 | ||||
| ### Fixed | ||||
| - App Icon not filled ([@AntsyLich](https://github.com/AntsyLich)) ([`1849715`](https://github.com/mihonapp/mihon/commit/18497154183356bb0d469b27827f9f7d6b7a3130)) | ||||
| - MangaUpdates default score being set to -1.0 ([@AntsyLich](https://github.com/AntsyLich)) ([`99fd273`](https://github.com/mihonapp/mihon/commit/99fd2731f5d9d374700e89fa67d4d5bf611bbafa)) | ||||
|  | ||||
| ## [v0.16.0] - 2024-01-16 | ||||
|  | ||||
| "The end of 立ち読み (Tachiyomi) is the beginning of みほん (Mihon)" | ||||
| Credit to LinkCable, the icon designer, for this poetic quote. | ||||
|  | ||||
| What's New? | ||||
| Well, nothing, except you now you need Android 8+ to install the app. | ||||
|  | ||||
| [unreleased]: https://github.com/mihonapp/mihon/compare/v0.16.5...HEAD | ||||
| [v0.16.5]: https://github.com/mihonapp/mihon/compare/v0.16.4...v0.16.5 | ||||
| [v0.16.4]: https://github.com/mihonapp/mihon/compare/v0.16.3...v0.16.4 | ||||
| [v0.16.3]: https://github.com/mihonapp/mihon/compare/v0.16.2...v0.16.3 | ||||
| [v0.16.2]: https://github.com/mihonapp/mihon/compare/v0.16.1...v0.16.2 | ||||
| [v0.16.1]: https://github.com/mihonapp/mihon/compare/v0.16.0...v0.16.1 | ||||
| [v0.16.0]: https://github.com/mihonapp/mihon/releases/tag/v0.16.0 | ||||
| @@ -1,126 +0,0 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| We as members, contributors, and leaders pledge to make participation in our | ||||
| community a harassment-free experience for everyone, regardless of age, body | ||||
| size, visible or invisible disability, ethnicity, sex characteristics, gender | ||||
| identity and expression, level of experience, education, socio-economic status, | ||||
| nationality, personal appearance, race, caste, color, religion, or sexual identity | ||||
| and orientation. | ||||
|  | ||||
| We pledge to act and interact in ways that contribute to an open, welcoming, | ||||
| diverse, inclusive, and healthy community. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to a positive environment for our | ||||
| community include: | ||||
|  | ||||
| * Demonstrating empathy and kindness toward other people | ||||
| * Being respectful of differing opinions, viewpoints, and experiences | ||||
| * Giving and gracefully accepting constructive feedback | ||||
| * Accepting responsibility and apologizing to those affected by our mistakes, | ||||
|   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 include: | ||||
|  | ||||
| * The use of sexualized language or imagery, and sexual attention or | ||||
|   advances of any kind | ||||
| * Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| * Public or private harassment | ||||
| * Publishing others' private information, such as a physical or email | ||||
|   address, without their explicit permission | ||||
| * Other conduct which could reasonably be considered inappropriate in a | ||||
|   professional setting | ||||
|  | ||||
| ## Enforcement Responsibilities | ||||
|  | ||||
| Community moderators are responsible for clarifying and enforcing our standards of | ||||
| acceptable behavior and will take appropriate and fair corrective action in | ||||
| response to any behavior that they deem inappropriate, threatening, offensive, | ||||
| or harmful. | ||||
|  | ||||
| Community moderators have the right and responsibility to remove, edit, or reject | ||||
| comments, commits, code, wiki edits, issues, and other contributions that are | ||||
| not aligned to this Code of Conduct, and will communicate reasons for moderation | ||||
| decisions when appropriate. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies within all community spaces, and also applies when | ||||
| an individual is officially representing the community in public spaces. | ||||
| Examples of representing our community include using an official e-mail address, | ||||
| posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported to the community moderators responsible for enforcement at | ||||
| the [Mihon Discord server](https://discord.gg/mihon). | ||||
| All complaints will be reviewed and investigated promptly and fairly. | ||||
|  | ||||
| All community moderators are obligated to respect the privacy and security of the | ||||
| reporter of any incident. | ||||
|  | ||||
| ## 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 | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), | ||||
| version 2.1, available at | ||||
| [v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). | ||||
|  | ||||
| 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 the FAQ at | ||||
| [FAQ](https://www.contributor-covenant.org/faq). Translations are available | ||||
| at [translations](https://www.contributor-covenant.org/translations). | ||||
| @@ -1,49 +0,0 @@ | ||||
| Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/mihonapp/mihon#issues-feature-requests-and-contributing). | ||||
|  | ||||
| --- | ||||
|  | ||||
| Thanks for your interest in contributing to Mihon! | ||||
|  | ||||
|  | ||||
| # Code contributions | ||||
|  | ||||
| Pull requests are welcome! | ||||
|  | ||||
| If you're interested in taking on [an open issue](https://github.com/mihonapp/mihon/issues), please comment on it so others are aware. | ||||
| You do not need to ask for permission nor an assignment. | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you. | ||||
|  | ||||
| - Basic [Android development](https://developer.android.com/) | ||||
| - [Kotlin](https://kotlinlang.org/) | ||||
|  | ||||
| ### Tools | ||||
|  | ||||
| - [Android Studio](https://developer.android.com/studio) | ||||
| - Emulator or phone with developer options enabled to test changes. | ||||
|  | ||||
| ## Getting help | ||||
|  | ||||
| - Join [the Discord server](https://discord.gg/mihon) for online help and to ask questions while developing. | ||||
|  | ||||
| # Translations | ||||
|  | ||||
| Translations are done externally via Weblate. See [our website](https://mihon.app/docs/contribute#translation) for more details. | ||||
|  | ||||
|  | ||||
| # Forks | ||||
|  | ||||
| Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/mihonapp/mihon/blob/main/LICENSE). | ||||
|  | ||||
| When creating a fork, remember to: | ||||
|  | ||||
| - To avoid confusion with the main app: | ||||
|     - Change the app name | ||||
|     - Change the app icon | ||||
|     - Change or disable the [app update checker](https://github.com/mihonapp/mihon/blob/main/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt) | ||||
| - To avoid installation conflicts: | ||||
|     - Change the `applicationId` in [`build.gradle.kts`](https://github.com/mihonapp/mihon/blob/main/app/build.gradle.kts) | ||||
| - To avoid having your data polluting the main app's analytics and crash report services: | ||||
|     - If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/mihonapp/mihon/blob/main/app/src/standard/google-services.json) with your own | ||||
							
								
								
									
										26
									
								
								LICENSE
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -174,3 +174,29 @@ | ||||
|       of your accepting any such warranty or additional liability. | ||||
|  | ||||
|    END OF TERMS AND CONDITIONS | ||||
|  | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
|  | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "{}" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
|  | ||||
|    Copyright {yyyy} {name of copyright owner} | ||||
|  | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
|  | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
|  | ||||
|   | ||||
							
								
								
									
										87
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,86 +1,3 @@ | ||||
| <div align="center"> | ||||
| I haven't started a readme | ||||
|  | ||||
| <a href="https://mihon.app"> | ||||
|     <img src="./.github/assets/logo.png" alt="Mihon logo" title="Mihon logo" width="80"/> | ||||
| </a> | ||||
|  | ||||
| # Mihon [App](#) | ||||
|  | ||||
| ### Full-featured reader | ||||
| Discover and read manga, webtoons, comics, and more – easier than ever on your Android device. | ||||
|  | ||||
| [](https://discord.gg/mihon) | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
|  | ||||
| [](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml) | ||||
| [](/LICENSE) | ||||
| [](https://hosted.weblate.org/engage/mihon/) | ||||
|  | ||||
| ## Download | ||||
|  | ||||
| [](https://github.com/mihonapp/mihon/releases) | ||||
| [](https://github.com/mihonapp/mihon-preview/releases) | ||||
|  | ||||
| *Requires Android 8.0 or higher.* | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| <div align="left"> | ||||
|  | ||||
| * Local reading of content. | ||||
| * A configurable reader with multiple viewers, reading directions and other settings. | ||||
| * Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support. | ||||
| * Categories to organize your library. | ||||
| * Light and dark themes. | ||||
| * Schedule updating your library for new chapters. | ||||
| * Create backups locally to read offline or to your desired cloud service. | ||||
| * Plus much more... | ||||
|  | ||||
| </div> | ||||
|  | ||||
| ## Contributing | ||||
|  | ||||
| [Code of conduct](./CODE_OF_CONDUCT.md) · [Contributing guide](./CONTRIBUTING.md) | ||||
|  | ||||
| Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. | ||||
|  | ||||
| Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/faq/general), the [changelog](https://mihon.app/changelogs/) and the already opened [issues](https://github.com/mihonapp/mihon/issues); if you got any questions, join our [Discord server](https://discord.gg/mihon). | ||||
|  | ||||
|  | ||||
| ### Repositories | ||||
|  | ||||
| [](https://github.com/mihonapp/website/) | ||||
| [](https://github.com/mihonapp/bitmap.kt/) | ||||
|  | ||||
| ### Credits | ||||
|  | ||||
| Thank you to all the people who have contributed! | ||||
|  | ||||
| <a href="https://github.com/mihonapp/mihon/graphs/contributors"> | ||||
|     <img src="https://contrib.rocks/image?repo=mihonapp/mihon" alt="Mihon app contributors" title="Mihon app contributors" width="800"/> | ||||
| </a> | ||||
|  | ||||
| ### Disclaimer | ||||
|  | ||||
| The developer(s) of this application does not have any affiliation with the content providers available, and this application hosts zero content. | ||||
|  | ||||
| ### License | ||||
|  | ||||
| <pre> | ||||
| Copyright © 2015 Javier Tomás | ||||
| Copyright © 2024 The Mihon Open Source Project | ||||
|  | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
|  | ||||
| http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| </pre> | ||||
|  | ||||
| </div> | ||||
| Automated Preview Builds(with updater): https://github.com/jobobby04/TachiyomiSYPreview/releases | ||||
|   | ||||
							
								
								
									
										3
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -1,3 +1,6 @@ | ||||
| /build | ||||
| *iml | ||||
| *.iml | ||||
| custom.gradle | ||||
| google-services.json | ||||
| output.json | ||||
							
								
								
									
										374
									
								
								app/build.gradle
									
									
									
									
									
										Executable file
									
								
							
							
						
						| @@ -0,0 +1,374 @@ | ||||
| import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile | ||||
| //noinspection GradleDependency | ||||
| import java.text.SimpleDateFormat | ||||
|  | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'com.google.android.gms.oss-licenses-plugin' | ||||
| apply plugin: 'kotlin-android' | ||||
| apply plugin: 'kotlin-android-extensions' | ||||
| apply plugin: 'kotlin-kapt' | ||||
| apply plugin: 'com.github.zellius.shortcut-helper' | ||||
| // Realm (EH) | ||||
| apply plugin: 'realm-android' | ||||
|  | ||||
| shortcutHelper.filePath = './shortcuts.xml' | ||||
|  | ||||
| ext { | ||||
|     // Git is needed in your system PATH for these commands to work. | ||||
|     // If it's not installed, you can return a random value as a workaround | ||||
|     getCommitCount = { | ||||
|         return 'git rev-list --count HEAD'.execute().text.trim() | ||||
|         // return "1" | ||||
|     } | ||||
|  | ||||
|     getGitSha = { | ||||
|         return 'git rev-parse --short HEAD'.execute().text.trim() | ||||
|         // return "1" | ||||
|     } | ||||
|  | ||||
|     getBuildTime = { | ||||
|         def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") | ||||
|         df.setTimeZone(TimeZone.getTimeZone("UTC")) | ||||
|         return df.format(new Date()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 29 | ||||
|     buildToolsVersion '29.0.3' | ||||
|     publishNonDefault true | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId "eu.kanade.tachiyomi.sy" | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 29 | ||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||
|         versionCode 1 | ||||
|         versionName "0.9.2.7" | ||||
|  | ||||
|         buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" | ||||
|         buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" | ||||
|         buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" | ||||
|         buildConfigField "boolean", "INCLUDE_UPDATER", "true" | ||||
|  | ||||
|         multiDexEnabled true | ||||
|  | ||||
|         ndk { | ||||
|             abiFilters "armeabi-v7a", "arm64-v8a", "x86" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     viewBinding { | ||||
|         enabled = true | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             versionNameSuffix "-${getCommitCount()}" | ||||
|             applicationIdSuffix ".debug" | ||||
|             ext.enableCrashlytics = false | ||||
|         } | ||||
|         releaseTest { | ||||
|             applicationIdSuffix ".rt" | ||||
| //            minifyEnabled true | ||||
| //            shrinkResources true | ||||
|             zipAlignEnabled true | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|         release { | ||||
|             minifyEnabled true | ||||
|             shrinkResources true | ||||
|             zipAlignEnabled true | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     flavorDimensions "default" | ||||
|  | ||||
|     productFlavors { | ||||
|         standard { | ||||
|             buildConfigField "boolean", "INCLUDE_UPDATER", "true" | ||||
|             dimension "default" | ||||
|         } | ||||
|         dev { | ||||
|             resConfigs "en", "xxhdpi" | ||||
|             dimension "default" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     compileOptions { | ||||
|         sourceCompatibility 1.8 | ||||
|         targetCompatibility 1.8 | ||||
|     } | ||||
|  | ||||
|     packagingOptions { | ||||
|         exclude 'META-INF/DEPENDENCIES' | ||||
|         exclude 'LICENSE.txt' | ||||
|         exclude 'META-INF/LICENSE' | ||||
|         exclude 'META-INF/LICENSE.txt' | ||||
|         exclude 'META-INF/NOTICE' | ||||
|         exclude 'META-INF/*.kotlin_module' | ||||
|  | ||||
|         // Compatibility for two RxJava versions (EXH) | ||||
|         exclude 'META-INF/rxjava.properties' | ||||
|     } | ||||
|  | ||||
|     lintOptions { | ||||
|         abortOnError false | ||||
|         checkReleaseBuilds false | ||||
|     } | ||||
|  | ||||
|     compileOptions { | ||||
|         sourceCompatibility = 1.8 | ||||
|         targetCompatibility = 1.8 | ||||
|     } | ||||
|  | ||||
|     kotlinOptions { | ||||
|         jvmTarget = "1.8" | ||||
|     } | ||||
| } | ||||
|  | ||||
| androidExtensions { | ||||
|     experimental = true | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|  | ||||
|     // Modified dependencies | ||||
|     implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7' | ||||
|     implementation 'com.github.inorichi:junrar-android:634c1f5' | ||||
|  | ||||
|     // Android support library | ||||
|     implementation 'androidx.appcompat:appcompat:1.1.0' | ||||
|     implementation 'androidx.cardview:cardview:1.0.0' | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||
|     implementation 'androidx.recyclerview:recyclerview:1.1.0' | ||||
|     implementation 'androidx.preference:preference:1.1.1' | ||||
|     implementation 'androidx.annotation:annotation:1.1.0' | ||||
|     implementation 'androidx.browser:browser:1.2.0' | ||||
|     implementation 'androidx.multidex:multidex:2.0.1' | ||||
|     implementation 'androidx.biometric:biometric:1.0.1' | ||||
|  | ||||
|     final lifecycle_version = '2.2.0' | ||||
|     implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" | ||||
|     implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" | ||||
|     implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" | ||||
|  | ||||
|     // UI library | ||||
|     implementation 'com.google.android.material:material:1.1.0' | ||||
|  | ||||
|     standardImplementation 'com.google.firebase:firebase-core:17.4.1' | ||||
|  | ||||
|     // ReactiveX | ||||
|     implementation 'io.reactivex:rxandroid:1.2.1' | ||||
|     implementation 'io.reactivex:rxjava:1.3.8' | ||||
|     implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' | ||||
|     implementation 'com.github.pwittchen:reactivenetwork:0.13.0' | ||||
|  | ||||
|     // Network client | ||||
|     final okhttp_version = '4.7.2' | ||||
|     implementation "com.squareup.okhttp3:okhttp:$okhttp_version" | ||||
|     implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" | ||||
|     implementation 'com.squareup.okio:okio:2.6.0' | ||||
|  | ||||
|     // REST | ||||
|     final retrofit_version = '2.9.0' | ||||
|     implementation "com.squareup.retrofit2:retrofit:$retrofit_version" | ||||
|     implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" | ||||
|     implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" | ||||
|  | ||||
|     // JSON | ||||
|     implementation 'com.google.code.gson:gson:2.8.6' | ||||
|     implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' | ||||
|  | ||||
|     // JavaScript engine | ||||
|     implementation 'com.squareup.duktape:duktape-android:1.2.0' // Stuck on 1.2.0 to fix MangaPlus extension | ||||
|  | ||||
|     // Disk | ||||
|     implementation 'com.jakewharton:disklrucache:2.0.2' | ||||
|     implementation 'com.github.inorichi:unifile:e9ee588' | ||||
|  | ||||
|     // HTML parser | ||||
|     implementation 'org.jsoup:jsoup:1.13.1' | ||||
|  | ||||
|     // Job scheduling | ||||
|     final work_version = '2.3.4' | ||||
|     implementation "androidx.work:work-runtime:$work_version" | ||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||
|  | ||||
|     // [EXH] Android 7 SSL Workaround | ||||
|     implementation 'com.google.android.gms:play-services-safetynet:17.0.0' | ||||
|  | ||||
|     // Changelog | ||||
|     implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' | ||||
|  | ||||
|     // Database | ||||
|     implementation 'androidx.sqlite:sqlite:2.1.0' | ||||
|     implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' | ||||
|     implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' | ||||
|     implementation 'io.requery:sqlite-android:3.31.0' | ||||
|  | ||||
|     // Preferences | ||||
|     implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' | ||||
|     implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1' | ||||
|  | ||||
|     // Model View Presenter | ||||
|     final nucleus_version = '3.0.0' | ||||
|     implementation "info.android15.nucleus:nucleus:$nucleus_version" | ||||
|     implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version" | ||||
|  | ||||
|     // Dependency injection | ||||
|     implementation "com.github.inorichi.injekt:injekt-core:65b0440" | ||||
|  | ||||
|     // Image library | ||||
|     final glide_version = '4.10.0' | ||||
|     implementation "com.github.bumptech.glide:glide:$glide_version" | ||||
|     implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" | ||||
|     kapt "com.github.bumptech.glide:compiler:$glide_version" | ||||
|  | ||||
|     // Logging | ||||
|     implementation 'com.jakewharton.timber:timber:4.7.1' | ||||
|  | ||||
|     // Crash reports | ||||
|     final acra_version = '5.5.0' | ||||
|     implementation "ch.acra:acra-http:$acra_version" | ||||
|  | ||||
|     // Sort | ||||
|     implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' | ||||
|  | ||||
|     // UI | ||||
|     implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' | ||||
|     implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' | ||||
|     implementation 'eu.davidea:flexible-adapter:5.1.0' | ||||
|     implementation 'eu.davidea:flexible-adapter-ui:1.0.0' | ||||
|     implementation 'com.nononsenseapps:filepicker:2.5.2' | ||||
|     implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0' | ||||
|     implementation 'com.github.mthli:Slice:v1.3' | ||||
|     implementation 'com.github.chrisbanes:PhotoView:2.3.0' | ||||
|     implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a' | ||||
|  | ||||
|     // 3.2.0+ introduces weird UI blinking or cut off issues on some devices | ||||
|     final material_dialogs_version = '3.1.1' | ||||
|     implementation "com.afollestad.material-dialogs:core:$material_dialogs_version" | ||||
|     implementation "com.afollestad.material-dialogs:input:$material_dialogs_version" | ||||
|     implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version" | ||||
|  | ||||
|     // Conductor | ||||
|     implementation 'com.bluelinelabs:conductor:2.1.5' | ||||
|     implementation("com.bluelinelabs:conductor-support:2.1.5") { | ||||
|         exclude group: "com.android.support" | ||||
|     } | ||||
|     implementation 'com.github.inorichi:conductor-support-preference:a32c357' | ||||
|  | ||||
|     // FlowBinding | ||||
|     final flowbinding_version = '0.11.1' | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version" | ||||
|  | ||||
|     // Tests | ||||
|     testImplementation 'junit:junit:4.13' | ||||
|     testImplementation 'org.assertj:assertj-core:3.12.2' | ||||
|     testImplementation 'org.mockito:mockito-core:1.10.19' | ||||
|  | ||||
|     final robolectric_version = '3.1.4' | ||||
|     testImplementation "org.robolectric:robolectric:$robolectric_version" | ||||
|     testImplementation "org.robolectric:shadows-multidex:$robolectric_version" | ||||
|     testImplementation "org.robolectric:shadows-play-services:$robolectric_version" | ||||
|  | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" | ||||
|  | ||||
|     final coroutines_version = '1.3.7' | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" | ||||
|  | ||||
|     implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' | ||||
|  | ||||
|     // Text distance (EH) | ||||
|     implementation 'info.debatty:java-string-similarity:1.2.1' | ||||
|  | ||||
|     // Pin lock view (EH) | ||||
|     implementation 'com.github.jawnnypoo:pinlockview:2.2.0' | ||||
|  | ||||
|     // Reprint (EH) | ||||
|     implementation 'com.github.ajalt.reprint:core:3.2.1@aar' | ||||
|     implementation 'com.github.ajalt.reprint:rxjava:3.2.1@aar' // optional: the RxJava 1 interface | ||||
|  | ||||
|     // Swirl (EH) | ||||
|     implementation 'com.mattprecious.swirl:swirl:1.2.0' | ||||
|  | ||||
|     // RxJava 2 interop for Realm (EH) | ||||
|     implementation 'com.github.akarnokd:rxjava2-interop:0.13.7' | ||||
|  | ||||
|     // Firebase (EH) | ||||
|     implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' | ||||
|  | ||||
|     // Better logging (EH) | ||||
|     implementation 'com.elvishew:xlog:1.6.1' | ||||
|  | ||||
|     // Time utils (EH) | ||||
|     def typed_time_version = '1.0.2' | ||||
|     implementation "com.github.kizitonwose.time:time:$typed_time_version" | ||||
|     implementation "com.github.kizitonwose.time:time-android:$typed_time_version" | ||||
|  | ||||
|     // Debug utils (EH) | ||||
|     debugImplementation 'com.ms-square:debugoverlay:1.1.3' | ||||
|     releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3' | ||||
|     releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3' | ||||
|     testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3' | ||||
|  | ||||
|     // Humanize (EH) | ||||
|     implementation 'com.github.mfornos:humanize-slim:1.2.2' | ||||
|  | ||||
|     implementation 'androidx.gridlayout:gridlayout:1.0.0' | ||||
|  | ||||
|     final def markwon_version = '4.1.0' | ||||
|  | ||||
|     implementation "io.noties.markwon:core:$markwon_version" | ||||
|     implementation "io.noties.markwon:ext-strikethrough:$markwon_version" | ||||
|     implementation "io.noties.markwon:ext-tables:$markwon_version" | ||||
|     implementation "io.noties.markwon:html:$markwon_version" | ||||
|     implementation "io.noties.markwon:image:$markwon_version" | ||||
|     implementation "io.noties.markwon:linkify:$markwon_version" | ||||
|  | ||||
|     implementation 'com.google.guava:guava:27.0.1-android' | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|     ext.kotlin_version = '1.3.72' | ||||
|     repositories { | ||||
|         mavenCentral() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|     } | ||||
| } | ||||
|  | ||||
| repositories { | ||||
|     mavenCentral() | ||||
| } | ||||
|  | ||||
| // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers | ||||
| tasks.withType(AbstractKotlinCompile).all { | ||||
|     kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] | ||||
| } | ||||
|  | ||||
| // Duplicating Hebrew string assets due to some locale code issues on different devices | ||||
| task copyResources(type: Copy) { | ||||
|     from './src/main/res/values-he' | ||||
|     into './src/main/res/values-iw' | ||||
|     include '**/*' | ||||
| } | ||||
|  | ||||
| preBuild.dependsOn(ktlintFormat, copyResources) | ||||
|  | ||||
| if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) { | ||||
|     apply plugin: 'com.google.gms.google-services' | ||||
|     // Firebase (EH) | ||||
|     apply plugin: 'io.fabric' | ||||
| } | ||||
| @@ -1,308 +0,0 @@ | ||||
| import mihon.buildlogic.getBuildTime | ||||
| import mihon.buildlogic.getCommitCount | ||||
| import mihon.buildlogic.getGitSha | ||||
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||||
|  | ||||
| plugins { | ||||
|     id("mihon.android.application") | ||||
|     id("mihon.android.application.compose") | ||||
|     id("com.github.zellius.shortcut-helper") | ||||
|     kotlin("plugin.serialization") | ||||
|     alias(libs.plugins.aboutLibraries) | ||||
| } | ||||
|  | ||||
| if (gradle.startParameter.taskRequests.toString().contains("Standard")) { | ||||
|     pluginManager.apply { | ||||
|         apply(libs.plugins.google.services.get().pluginId) | ||||
|         apply(libs.plugins.firebase.crashlytics.get().pluginId) | ||||
|     } | ||||
| } | ||||
|  | ||||
| shortcutHelper.setFilePath("./shortcuts.xml") | ||||
|  | ||||
| val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") | ||||
|  | ||||
| android { | ||||
|     namespace = "eu.kanade.tachiyomi" | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId = "app.mihon" | ||||
|  | ||||
|         versionCode = 7 | ||||
|         versionName = "0.16.5" | ||||
|  | ||||
|         buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") | ||||
|         buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") | ||||
|         buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"") | ||||
|         buildConfigField("boolean", "INCLUDE_UPDATER", "false") | ||||
|         buildConfigField("boolean", "PREVIEW", "false") | ||||
|  | ||||
|         ndk { | ||||
|             abiFilters += supportedAbis | ||||
|         } | ||||
|  | ||||
|         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||||
|     } | ||||
|  | ||||
|     splits { | ||||
|         abi { | ||||
|             isEnable = true | ||||
|             reset() | ||||
|             include(*supportedAbis.toTypedArray()) | ||||
|             isUniversalApk = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         named("debug") { | ||||
|             versionNameSuffix = "-${getCommitCount()}" | ||||
|             applicationIdSuffix = ".debug" | ||||
|             isPseudoLocalesEnabled = true | ||||
|         } | ||||
|         named("release") { | ||||
|             isShrinkResources = true | ||||
|             isMinifyEnabled = true | ||||
|             proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro") | ||||
|         } | ||||
|         create("preview") { | ||||
|             initWith(getByName("release")) | ||||
|             buildConfigField("boolean", "PREVIEW", "true") | ||||
|  | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             val debugType = getByName("debug") | ||||
|             versionNameSuffix = debugType.versionNameSuffix | ||||
|             applicationIdSuffix = debugType.applicationIdSuffix | ||||
|         } | ||||
|         create("benchmark") { | ||||
|             initWith(getByName("release")) | ||||
|  | ||||
|             signingConfig = signingConfigs.getByName("debug") | ||||
|             matchingFallbacks.add("release") | ||||
|             isDebuggable = false | ||||
|             isProfileable = true | ||||
|             versionNameSuffix = "-benchmark" | ||||
|             applicationIdSuffix = ".benchmark" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     sourceSets { | ||||
|         getByName("preview").res.srcDirs("src/debug/res") | ||||
|         getByName("benchmark").res.srcDirs("src/debug/res") | ||||
|     } | ||||
|  | ||||
|     flavorDimensions.add("default") | ||||
|  | ||||
|     productFlavors { | ||||
|         create("standard") { | ||||
|             buildConfigField("boolean", "INCLUDE_UPDATER", "true") | ||||
|             dimension = "default" | ||||
|         } | ||||
|         create("dev") { | ||||
|             // Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales | ||||
|             resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi")) | ||||
|             dimension = "default" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     packaging { | ||||
|         resources.excludes.addAll( | ||||
|             listOf( | ||||
|                 "kotlin-tooling-metadata.json", | ||||
|                 "META-INF/DEPENDENCIES", | ||||
|                 "LICENSE.txt", | ||||
|                 "META-INF/LICENSE", | ||||
|                 "META-INF/**/LICENSE.txt", | ||||
|                 "META-INF/*.properties", | ||||
|                 "META-INF/**/*.properties", | ||||
|                 "META-INF/README.md", | ||||
|                 "META-INF/NOTICE", | ||||
|                 "META-INF/*.version", | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     dependenciesInfo { | ||||
|         includeInApk = false | ||||
|     } | ||||
|  | ||||
|     buildFeatures { | ||||
|         viewBinding = true | ||||
|         buildConfig = true | ||||
|  | ||||
|         // Disable some unused things | ||||
|         aidl = false | ||||
|         renderScript = false | ||||
|         shaders = false | ||||
|     } | ||||
|  | ||||
|     lint { | ||||
|         abortOnError = false | ||||
|         checkReleaseBuilds = false | ||||
|     } | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     implementation(projects.i18n) | ||||
|     implementation(projects.core.archive) | ||||
|     implementation(projects.core.common) | ||||
|     implementation(projects.coreMetadata) | ||||
|     implementation(projects.sourceApi) | ||||
|     implementation(projects.sourceLocal) | ||||
|     implementation(projects.data) | ||||
|     implementation(projects.domain) | ||||
|     implementation(projects.presentationCore) | ||||
|     implementation(projects.presentationWidget) | ||||
|  | ||||
|     // Compose | ||||
|     implementation(compose.activity) | ||||
|     implementation(compose.foundation) | ||||
|     implementation(compose.material3.core) | ||||
|     implementation(compose.material.icons) | ||||
|     implementation(compose.animation) | ||||
|     implementation(compose.animation.graphics) | ||||
|     debugImplementation(compose.ui.tooling) | ||||
|     implementation(compose.ui.tooling.preview) | ||||
|     implementation(compose.ui.util) | ||||
|  | ||||
|     implementation(androidx.interpolator) | ||||
|  | ||||
|     implementation(androidx.paging.runtime) | ||||
|     implementation(androidx.paging.compose) | ||||
|  | ||||
|     implementation(libs.bundles.sqlite) | ||||
|  | ||||
|     implementation(kotlinx.reflect) | ||||
|     implementation(kotlinx.immutables) | ||||
|  | ||||
|     implementation(platform(kotlinx.coroutines.bom)) | ||||
|     implementation(kotlinx.bundles.coroutines) | ||||
|  | ||||
|     // AndroidX libraries | ||||
|     implementation(androidx.annotation) | ||||
|     implementation(androidx.appcompat) | ||||
|     implementation(androidx.biometricktx) | ||||
|     implementation(androidx.constraintlayout) | ||||
|     implementation(androidx.corektx) | ||||
|     implementation(androidx.splashscreen) | ||||
|     implementation(androidx.recyclerview) | ||||
|     implementation(androidx.viewpager) | ||||
|     implementation(androidx.profileinstaller) | ||||
|  | ||||
|     implementation(androidx.bundles.lifecycle) | ||||
|  | ||||
|     // Job scheduling | ||||
|     implementation(androidx.workmanager) | ||||
|  | ||||
|     // RxJava | ||||
|     implementation(libs.rxjava) | ||||
|  | ||||
|     // Networking | ||||
|     implementation(libs.bundles.okhttp) | ||||
|     implementation(libs.okio) | ||||
|     implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10 | ||||
|  | ||||
|     // Data serialization (JSON, protobuf, xml) | ||||
|     implementation(kotlinx.bundles.serialization) | ||||
|  | ||||
|     // HTML parser | ||||
|     implementation(libs.jsoup) | ||||
|  | ||||
|     // Disk | ||||
|     implementation(libs.disklrucache) | ||||
|     implementation(libs.unifile) | ||||
|  | ||||
|     // Preferences | ||||
|     implementation(libs.preferencektx) | ||||
|  | ||||
|     // Dependency injection | ||||
|     implementation(libs.injekt) | ||||
|  | ||||
|     // Image loading | ||||
|     implementation(platform(libs.coil.bom)) | ||||
|     implementation(libs.bundles.coil) | ||||
|     implementation(libs.subsamplingscaleimageview) { | ||||
|         exclude(module = "image-decoder") | ||||
|     } | ||||
|     implementation(libs.image.decoder) | ||||
|  | ||||
|     // UI libraries | ||||
|     implementation(libs.material) | ||||
|     implementation(libs.flexible.adapter.core) | ||||
|     implementation(libs.photoview) | ||||
|     implementation(libs.directionalviewpager) { | ||||
|         exclude(group = "androidx.viewpager", module = "viewpager") | ||||
|     } | ||||
|     implementation(libs.insetter) | ||||
|     implementation(libs.bundles.richtext) | ||||
|     implementation(libs.aboutLibraries.compose) | ||||
|     implementation(libs.bundles.voyager) | ||||
|     implementation(libs.compose.materialmotion) | ||||
|     implementation(libs.swipe) | ||||
|     implementation(libs.compose.webview) | ||||
|     implementation(libs.compose.grid) | ||||
|  | ||||
|     // Logging | ||||
|     implementation(libs.logcat) | ||||
|  | ||||
|     // Crash reports/analytics | ||||
|     "standardImplementation"(platform(libs.firebase.bom)) | ||||
|     "standardImplementation"(libs.firebase.analytics) | ||||
|     "standardImplementation"(libs.firebase.crashlytics) | ||||
|  | ||||
|     // Shizuku | ||||
|     implementation(libs.bundles.shizuku) | ||||
|  | ||||
|     // Tests | ||||
|     testImplementation(libs.bundles.test) | ||||
|  | ||||
|     // For detecting memory leaks; see https://square.github.io/leakcanary/ | ||||
|     // debugImplementation(libs.leakcanary.android) | ||||
|     implementation(libs.leakcanary.plumber) | ||||
|  | ||||
|     testImplementation(kotlinx.coroutines.test) | ||||
| } | ||||
|  | ||||
| androidComponents { | ||||
|     beforeVariants { variantBuilder -> | ||||
|         // Disables standardBenchmark | ||||
|         if (variantBuilder.buildType == "benchmark") { | ||||
|             variantBuilder.enable = variantBuilder.productFlavors.containsAll( | ||||
|                 listOf("default" to "dev"), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     onVariants(selector().withFlavor("default" to "standard")) { | ||||
|         // Only excluding in standard flavor because this breaks | ||||
|         // Layout Inspector's Compose tree | ||||
|         it.packaging.resources.excludes.add("META-INF/*.version") | ||||
|     } | ||||
| } | ||||
|  | ||||
| tasks { | ||||
|     // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) | ||||
|     withType<KotlinCompile> { | ||||
|         compilerOptions.freeCompilerArgs.addAll( | ||||
|             "-Xcontext-receivers", | ||||
|             "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", | ||||
|             "-opt-in=androidx.compose.material.ExperimentalMaterialApi", | ||||
|             "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api", | ||||
|             "-opt-in=androidx.compose.material.ExperimentalMaterialApi", | ||||
|             "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", | ||||
|             "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", | ||||
|             "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", | ||||
|             "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", | ||||
|             "-opt-in=coil3.annotation.ExperimentalCoilApi", | ||||
|             "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", | ||||
|             "-opt-in=kotlinx.coroutines.FlowPreview", | ||||
|             "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", | ||||
|             "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|     dependencies { | ||||
|         classpath(kotlinx.gradle) | ||||
|     } | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| -dontusemixedcaseclassnames | ||||
| -ignorewarnings | ||||
| -verbose | ||||
|  | ||||
| -keepattributes *Annotation* | ||||
|  | ||||
| -keepclasseswithmembernames,includedescriptorclasses class * { | ||||
|     native <methods>; | ||||
| } | ||||
|  | ||||
| -keepclassmembers enum * { | ||||
|     public static **[] values(); | ||||
|     public static ** valueOf(java.lang.String); | ||||
| } | ||||
|  | ||||
| -keepclassmembers class * implements android.os.Parcelable { | ||||
|     public static final ** CREATOR; | ||||
| } | ||||
|  | ||||
| -keep class androidx.annotation.Keep | ||||
|  | ||||
| -keep @androidx.annotation.Keep class * {*;} | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <methods>; | ||||
| } | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <fields>; | ||||
| } | ||||
|  | ||||
| -keepclasseswithmembers class * { | ||||
|     @androidx.annotation.Keep <init>(...); | ||||
| } | ||||
							
								
								
									
										252
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,31 +1,37 @@ | ||||
| -dontobfuscate | ||||
|  | ||||
| -keep,allowoptimization class eu.kanade.** | ||||
| -keep,allowoptimization class tachiyomi.** | ||||
| -keep,allowoptimization class mihon.** | ||||
| # Extensions may require methods unused in the core app | ||||
| -dontwarn eu.kanade.tachiyomi.** | ||||
| -keep class eu.kanade.tachiyomi.** { public protected private *; } | ||||
|  | ||||
| # Keep common dependencies used in extensions | ||||
| -keep,allowoptimization class androidx.preference.** { public protected *; } | ||||
| -keep,allowoptimization class kotlin.** { public protected *; } | ||||
| -keep,allowoptimization class kotlinx.coroutines.** { public protected *; } | ||||
| -keep,allowoptimization class kotlinx.serialization.** { public protected *; } | ||||
| -keep,allowoptimization class kotlin.time.** { public protected *; } | ||||
| -keep,allowoptimization class okhttp3.** { public protected *; } | ||||
| -keep,allowoptimization class okio.** { public protected *; } | ||||
| -keep,allowoptimization class org.jsoup.** { public protected *; } | ||||
| -keep,allowoptimization class rx.** { public protected *; } | ||||
| -keep,allowoptimization class app.cash.quickjs.** { public protected *; } | ||||
| -keep,allowoptimization class uy.kohesive.injekt.** { public protected *; } | ||||
| -keep class org.jsoup.** { *; } | ||||
| -keep class kotlin.** { *; } | ||||
| -keep class okhttp3.** { *; } | ||||
| -keep class com.google.gson.** { *; } | ||||
| -keep class com.github.salomonbrys.kotson.** { *; } | ||||
| -keep class com.squareup.duktape.** { *; } | ||||
|  | ||||
| # From extensions-lib | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; } | ||||
| -keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; } | ||||
| # === Keep EH classes | ||||
| -keep class exh.** { *; } | ||||
| -keep class xyz.nulldev.** { *; } | ||||
|  | ||||
| ##---------------Begin: proguard configuration for RxJava 1.x  ---------- | ||||
| # === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350 | ||||
| -keep class rx.android.** { *; } | ||||
|  | ||||
| # Design library | ||||
| -dontwarn com.google.android.material.** | ||||
| -keep class com.google.android.material.** { *; } | ||||
| -keep interface com.google.android.material.** { *; } | ||||
| -keep public class com.google.android.material.R$* { *; } | ||||
|  | ||||
| -keep class com.hippo.image.** { *; } | ||||
| -keep interface com.hippo.image.** { *; } | ||||
| -keepclassmembers class * extends nucleus.presenter.Presenter { | ||||
|     <init>(); | ||||
| } | ||||
|  | ||||
|  | ||||
| # RxJava 1.1.0 | ||||
| -dontwarn sun.misc.** | ||||
|  | ||||
| -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { | ||||
| @@ -42,41 +48,185 @@ | ||||
| } | ||||
|  | ||||
| -dontnote rx.internal.util.PlatformDependent | ||||
| ##---------------End: proguard configuration for RxJava 1.x  ---------- | ||||
|  | ||||
| ##---------------Begin: proguard configuration for okhttp  ---------- | ||||
| -keepclasseswithmembers class okhttp3.MultipartBody$Builder { *; } | ||||
| ##---------------End: proguard configuration for okhttp  ---------- | ||||
| # === Reactive network: https://github.com/pwittchen/ReactiveNetwork/tree/v0.12.4#proguard-configuration | ||||
| -dontwarn com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork | ||||
| -dontwarn io.reactivex.functions.Function | ||||
| -dontwarn rx.internal.util.** | ||||
| -dontwarn sun.misc.Unsafe | ||||
|  | ||||
| ##---------------Begin: proguard configuration for kotlinx.serialization  ---------- | ||||
| -keepattributes *Annotation*, InnerClasses | ||||
| -dontnote kotlinx.serialization.** # core serialization annotations | ||||
| # === Okhttp: https://github.com/square/okhttp/blob/3637fc56f70f87da696847defd311dbfb28e87b5/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro | ||||
| # JSR 305 annotations are for embedding nullability information. | ||||
| -dontwarn javax.annotation.** | ||||
| # A resource is loaded with a relative path so the package of this class must be preserved. | ||||
| -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase | ||||
| # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. | ||||
| -dontwarn org.codehaus.mojo.animal_sniffer.* | ||||
| # OkHttp platform used only on JVM and when Conscrypt dependency is available. | ||||
| -dontwarn okhttp3.internal.platform.ConscryptPlatform | ||||
|  | ||||
| # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer | ||||
| -keepclassmembers class kotlinx.serialization.json.** { | ||||
|     *** Companion; | ||||
| } | ||||
| -keepclasseswithmembers class kotlinx.serialization.json.** { | ||||
|     kotlinx.serialization.KSerializer serializer(...); | ||||
| # === Okio: https://github.com/square/okio/tree/9b8545e7fa267c9d89753283990f24a35cd69cd6#proguard | ||||
| -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, | ||||
| # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) | ||||
| -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>; | ||||
| } | ||||
|  | ||||
| -keep,includedescriptorclasses class eu.kanade.**$$serializer { *; } | ||||
| -keepclassmembers class eu.kanade.** { | ||||
|     *** Companion; | ||||
| } | ||||
| -keepclasseswithmembers class eu.kanade.** { | ||||
|     kotlinx.serialization.KSerializer serializer(...); | ||||
| # == Nucleus | ||||
| -keepclassmembers class * extends nucleus.presenter.Presenter { | ||||
|     <init>(); | ||||
| } | ||||
|  | ||||
| -keep class kotlinx.serialization.** | ||||
| -keepclassmembers class kotlinx.serialization.** { | ||||
|     <methods>; | ||||
| # TODO Changeloglib? Does it need proguard? | ||||
|  | ||||
| # === Injekt | ||||
| ## From original config: "Attempt to fix: java.lang.NoClassDefFoundError: uy.kohesive.injekt.registry.default.DefaultRegistrar$NOKEY$1" | ||||
| -keep class uy.kohesive.injekt.** { *; } | ||||
|  | ||||
|  | ||||
| # === Glide | ||||
| -keep public class * implements com.bumptech.glide.module.GlideModule | ||||
| -keep public class * extends com.bumptech.glide.module.AppGlideModule | ||||
| -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { | ||||
|   **[] $VALUES; | ||||
|   public *; | ||||
| } | ||||
| ##---------------End: proguard configuration for kotlinx.serialization  ---------- | ||||
|  | ||||
| # XmlUtil | ||||
| -keep public enum nl.adaptivity.xmlutil.EventType { *; } | ||||
| -dontwarn com.bumptech.glide.load.resource.bitmap.VideoDecoder | ||||
|  | ||||
| # Firebase | ||||
| -keep class com.google.firebase.installations.** { *; } | ||||
| -keep interface com.google.firebase.installations.** { *; } | ||||
| # === Glide-transformations: https://github.com/wasabeef/glide-transformations/blob/3aa8e53c6a51b8351d312f802ba1354c5b115168/transformations/proguard-rules.txt | ||||
| -dontwarn jp.co.cyberagent.android.gpuimage.** | ||||
|  | ||||
| # === Conductor | ||||
| # This isn't in the consumer proguard rules yet: https://github.com/bluelinelabs/Conductor/pull/550/files | ||||
| -keepclassmembers public class * extends com.bluelinelabs.conductor.ControllerChangeHandler { | ||||
|    public <init>(); | ||||
| } | ||||
|  | ||||
| # === RxBinding | ||||
| -dontwarn com.google.auto.value.AutoValue | ||||
|  | ||||
| # === Crashlytics | ||||
| -keepattributes *Annotation* | ||||
| -keepattributes SourceFile,LineNumberTable | ||||
| -keep class com.crashlytics.** { *; } | ||||
| -dontwarn com.crashlytics.** | ||||
|  | ||||
| # === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava | ||||
| -dontwarn javax.lang.model.element.Modifier | ||||
| -keep class org.ocpsoft.prettytime.i18n.** | ||||
|  | ||||
| # Note: We intentionally don't add the flags we'd need to make Enums work. | ||||
| # That's because the Proguard configuration required to make it work on | ||||
| # optimized code would preclude lots of optimization, like converting enums | ||||
| # into ints. | ||||
|  | ||||
| # Throwables uses internal APIs for lazy stack trace resolution | ||||
| -dontnote sun.misc.SharedSecrets | ||||
| -keep class sun.misc.SharedSecrets { | ||||
|   *** getJavaLangAccess(...); | ||||
| } | ||||
| -dontnote sun.misc.JavaLangAccess | ||||
| -keep class sun.misc.JavaLangAccess { | ||||
|   *** getStackTraceElement(...); | ||||
|   *** getStackTraceDepth(...); | ||||
| } | ||||
|  | ||||
| # FinalizableReferenceQueue calls this reflectively | ||||
| # Proguard is intelligent enough to spot the use of reflection onto this, so we | ||||
| # only need to keep the names, and allow it to be stripped out if | ||||
| # FinalizableReferenceQueue is unused. | ||||
| -keepnames class com.google.common.base.internal.Finalizer { | ||||
|   *** startFinalizer(...); | ||||
| } | ||||
| # However, it cannot "spot" that this method needs to be kept IF the class is. | ||||
| -keepclassmembers class com.google.common.base.internal.Finalizer { | ||||
|   *** startFinalizer(...); | ||||
| } | ||||
| -keepnames class com.google.common.base.FinalizableReference { | ||||
|   void finalizeReferent(); | ||||
| } | ||||
| -keepclassmembers class com.google.common.base.FinalizableReference { | ||||
|   void finalizeReferent(); | ||||
| } | ||||
|  | ||||
| # Striped64, LittleEndianByteArray, UnsignedBytes, AbstractFuture | ||||
| -dontwarn sun.misc.Unsafe | ||||
|  | ||||
| # Striped64 appears to make some assumptions about object layout that | ||||
| # really might not be safe. This should be investigated. | ||||
| -keepclassmembers class com.google.common.cache.Striped64 { | ||||
|   *** base; | ||||
|   *** busy; | ||||
| } | ||||
| -keepclassmembers class com.google.common.cache.Striped64$Cell { | ||||
|   <fields>; | ||||
| } | ||||
|  | ||||
| -dontwarn java.lang.SafeVarargs | ||||
|  | ||||
| -keep class java.lang.Throwable { | ||||
|   *** addSuppressed(...); | ||||
| } | ||||
|  | ||||
| # Futures.getChecked, in both of its variants, is incompatible with proguard. | ||||
|  | ||||
| # Used by AtomicReferenceFieldUpdater and sun.misc.Unsafe | ||||
| -keepclassmembers class com.google.common.util.concurrent.AbstractFuture** { | ||||
|   *** waiters; | ||||
|   *** value; | ||||
|   *** listeners; | ||||
|   *** thread; | ||||
|   *** next; | ||||
| } | ||||
| -keepclassmembers class com.google.common.util.concurrent.AtomicDouble { | ||||
|   *** value; | ||||
| } | ||||
| -keepclassmembers class com.google.common.util.concurrent.AggregateFutureState { | ||||
|   *** remaining; | ||||
|   *** seenExceptions; | ||||
| } | ||||
|  | ||||
| # Since Unsafe is using the field offsets of these inner classes, we don't want | ||||
| # to have class merging or similar tricks applied to these classes and their | ||||
| # fields. It's safe to allow obfuscation, since the by-name references are | ||||
| # already preserved in the -keep statement above. | ||||
| -keep,allowshrinking,allowobfuscation class com.google.common.util.concurrent.AbstractFuture** { | ||||
|   <fields>; | ||||
| } | ||||
|  | ||||
| # Futures.getChecked (which often won't work with Proguard anyway) uses this. It | ||||
| # has a fallback, but again, don't use Futures.getChecked on Android regardless. | ||||
| -dontwarn java.lang.ClassValue | ||||
|  | ||||
| # MoreExecutors references AppEngine | ||||
| -dontnote com.google.appengine.api.ThreadManager | ||||
| -keep class com.google.appengine.api.ThreadManager { | ||||
|   static *** currentRequestThreadFactory(...); | ||||
| } | ||||
| -dontnote com.google.apphosting.api.ApiProxy | ||||
| -keep class com.google.apphosting.api.ApiProxy { | ||||
|   static *** getCurrentEnvironment (...); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| <shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|  | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_collections_bookmark_48dp" | ||||
| @@ -16,7 +17,7 @@ | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_recently_updated" | ||||
|         android:shortcutLongLabel="@string/label_recent_updates" | ||||
|         android:shortcutShortLabel="@string/label_recent_updates"> | ||||
|         android:shortcutShortLabel="@string/short_recent_updates"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="432" | ||||
|     android:viewportHeight="432"> | ||||
|   <group> | ||||
|     <clip-path | ||||
|         android:pathData="M0,0h432v432h-432z"/> | ||||
|     <path | ||||
|         android:pathData="M0,0h432v432h-432z" | ||||
|         android:fillColor="#FAFAFA"/> | ||||
|     <path | ||||
|         android:pathData="M0,0h432v432h-432z" | ||||
|         android:fillColor="#2E3943"/> | ||||
|     <path | ||||
|         android:pathData="M322.13,215.5C322.13,272.66 274.64,319 216.07,319C157.49,319 110,272.66 110,215.5C110,158.34 157.49,112 216.07,112C274.64,112 322.13,158.34 322.13,215.5Z" | ||||
|         android:fillColor="#F2FAFF"/> | ||||
|     <path | ||||
|         android:pathData="M216.07,299.59C263.66,299.59 302.24,261.94 302.24,215.5C302.24,169.06 263.66,131.41 216.07,131.41C168.47,131.41 129.89,169.06 129.89,215.5C129.89,261.94 168.47,299.59 216.07,299.59ZM216.07,319C274.64,319 322.13,272.66 322.13,215.5C322.13,158.34 274.64,112 216.07,112C157.49,112 110,158.34 110,215.5C110,272.66 157.49,319 216.07,319Z" | ||||
|         android:fillColor="#7EBBED" | ||||
|         android:fillType="evenOdd"/> | ||||
|   </group> | ||||
| </vector> | ||||
| @@ -1,9 +1,97 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="108dp" | ||||
|     android:height="108dp" | ||||
|     android:viewportWidth="432" | ||||
|     android:viewportHeight="432"> | ||||
|   <path | ||||
|       android:pathData="M182.03,188.7L181.33,172.69C183.42,173.09 185.91,173.19 191.57,173.19C198.44,173.19 207.49,172.79 212.16,172.19C214.15,171.99 214.95,171.7 216.24,171L226.98,180.15C225.98,181.54 225.68,182.14 224.59,184.92C223.7,187.11 219.62,199.74 218.03,205.11C225.39,206.6 229.46,207.7 235.03,209.98C235.73,205.11 235.83,202.52 235.83,193.67C235.83,191.39 235.73,190.09 235.43,188.01L252.74,188.6C252.24,190.99 252.14,191.98 252.04,195.86C251.64,205.21 251.24,209.68 250.25,216.45C257.11,219.93 257.11,219.93 260.59,221.82C262.38,222.81 262.78,223.01 263.97,223.41L258.2,242.01C255.42,239.52 251.54,236.83 245.87,233.65C240.9,245.49 232.65,254.14 220.12,261C215.94,255.43 212.76,252.05 207.68,248.07C215.04,244.59 218.43,242.4 222.3,238.72C226.08,235.04 228.57,231.46 230.96,226.09C224.59,223.21 220.51,221.92 213.45,220.43C209.38,232.56 206.09,240.32 203.21,244.99C199.33,251.25 194.06,254.54 187.99,254.54C183.32,254.54 178.55,252.45 175.07,248.87C171.09,244.79 169,239.12 169,232.56C169,222.81 173.67,214.36 181.83,209.09C187.1,205.71 192.67,204.21 201.52,203.72C203.31,197.85 204.8,192.78 206.19,187.11C201.82,187.51 196.35,187.81 189.68,188.1C186.1,188.2 184.91,188.3 182.03,188.7ZM197.14,218.93C192.47,219.73 189.68,221.22 187.2,224.4C185.31,226.59 184.41,229.18 184.41,231.96C184.41,235.04 185.91,237.33 187.8,237.33C190.08,237.33 192.67,232.16 197.14,218.93Z" | ||||
|       android:fillColor="#031019"/> | ||||
|         android:width="108dp" | ||||
|         android:height="108dp" | ||||
|         android:viewportWidth="108.0" | ||||
|         android:viewportHeight="108.0"> | ||||
|     <path | ||||
|         android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z" | ||||
|         android:fillType="evenOdd" | ||||
|         android:fillColor="#000"/> | ||||
|     <path | ||||
|         android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z" | ||||
|         android:fillType="evenOdd" | ||||
|         android:fillColor="#455A64"/> | ||||
|     <path | ||||
|         android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z" | ||||
|         android:fillType="evenOdd" | ||||
|         android:fillColor="#607D8B"/> | ||||
|     <path | ||||
|         android:name="path_3" | ||||
|         android:pathData="M 54 54.5 M 28.5 54.5 C 28.5 47.74 31.188 41.249 35.969 36.469 C 40.749 31.688 47.24 29 54 29 C 60.76 29 67.251 31.688 72.031 36.469 C 76.812 41.249 79.5 47.74 79.5 54.5 C 79.5 61.26 76.812 67.751 72.031 72.531 C 67.251 77.312 60.76 80 54 80 C 47.24 80 40.749 77.312 35.969 72.531 C 31.188 67.751 28.5 61.26 28.5 54.5" | ||||
|         android:fillColor="#000" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_4" | ||||
|         android:pathData="M 54 54.5 M 28.5 54.5 C 28.5 47.74 31.188 41.249 35.969 36.469 C 40.749 31.688 47.24 29 54 29 C 60.76 29 67.251 31.688 72.031 36.469 C 76.812 41.249 79.5 47.74 79.5 54.5 C 79.5 61.26 76.812 67.751 72.031 72.531 C 67.251 77.312 60.76 80 54 80 C 47.24 80 40.749 77.312 35.969 72.531 C 31.188 67.751 28.5 61.26 28.5 54.5" | ||||
|         android:fillColor="#CE2828" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_5" | ||||
|         android:pathData="M 54 54.5 M 34.06 54.5 C 33.964 50.23 35.243 46.04 37.707 42.551 C 40.171 39.062 43.692 36.455 47.748 35.117 C 51.805 33.779 56.185 33.779 60.242 35.117 C 64.298 36.455 67.819 39.062 70.283 42.551 C 72.747 46.04 74.026 50.23 73.93 54.5 C 74.026 58.77 72.747 62.96 70.283 66.449 C 67.819 69.938 64.298 72.545 60.242 73.883 C 56.185 75.221 51.805 75.221 47.748 73.883 C 43.692 72.545 40.171 69.938 37.707 66.449 C 35.243 62.96 33.964 58.77 34.06 54.5" | ||||
|         android:fillColor="#FFF" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_6" | ||||
|         android:pathData="M 54.174 36.266 C 64.147 36.266 72.234 44.397 72.234 54.426 C 72.234 64.459 64.147 72.593 54.174 72.593 C 44.197 72.593 36.113 64.459 36.113 54.426 C 36.113 44.397 44.197 36.266 54.174 36.266 Z" | ||||
|         android:fillColor="#ffcc4d" | ||||
|         android:strokeColor="#ffcc4d" | ||||
|         android:strokeWidth="4.628571428571428" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round"/> | ||||
|     <path | ||||
|         android:name="path_7" | ||||
|         android:pathData="M 48.774 45.158 C 49.988 45.158 50.973 46.452 50.973 48.05 C 50.973 49.65 49.988 50.946 48.774 50.946 C 47.559 50.946 46.576 49.65 46.576 48.05 C 46.576 46.452 47.559 45.158 48.774 45.158 Z" | ||||
|         android:fillColor="#674600" | ||||
|         android:strokeColor="#674600" | ||||
|         android:strokeWidth="0.1" | ||||
|         android:strokeLineJoin="round"/> | ||||
|     <path | ||||
|         android:name="path_8" | ||||
|         android:pathData="M 62.02 45.158 C 63.235 45.158 64.219 46.452 64.219 48.05 C 64.219 49.65 63.235 50.946 62.02 50.946 C 60.805 50.946 59.821 49.65 59.821 48.05 C 59.821 46.452 60.805 45.158 62.02 45.158 Z" | ||||
|         android:fillColor="#674600" | ||||
|         android:strokeColor="#674600" | ||||
|         android:strokeWidth="0.1" | ||||
|         android:strokeLineJoin="round"/> | ||||
|     <path | ||||
|         android:name="path_9" | ||||
|         android:pathData="M 44.687 42.102 C 44.687 42.102 48.404 40.049 53.119 42.102" | ||||
|         android:strokeColor="#674600" | ||||
|         android:strokeWidth="2" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_10" | ||||
|         android:pathData="M 58.035 42.102 C 58.035 42.102 61.751 40.049 66.465 42.102" | ||||
|         android:strokeColor="#674600" | ||||
|         android:strokeWidth="2" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_11" | ||||
|         android:pathData="M 49.191 56.685 C 49.191 56.685 51.703 59.685 56.992 59.685" | ||||
|         android:strokeColor="#674600" | ||||
|         android:strokeWidth="2" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_12" | ||||
|         android:pathData="M 34.072 67.511 C 34.072 67.511 34.524 66.248 37.476 63.634 C 37.476 63.634 39.143 61.991 37.876 58.114 C 37.876 58.114 37.534 56.06 39.681 57.255 C 39.681 57.255 44.129 60.46 40.871 65 C 40.871 65 40.241 66.117 41.652 65.78 L 53.939 63.103 C 53.939 63.103 55.275 62.752 55.535 64.163 C 55.535 64.163 55.696 65.372 54.641 65.672 L 47.742 67.378 C 47.742 67.378 50.72 69.597 47.742 70.825 C 47.742 70.825 50.294 72.779 47.336 74.268 C 47.336 74.268 49.276 76.015 46.407 77.019 C 46.407 77.019 42.525 78.474 39.209 77.831 C 35.509 77.103 31.947 72.114 34.072 67.511 Z" | ||||
|         android:fillColor="#f4900c" | ||||
|         android:strokeColor="#f4900c" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round" | ||||
|         android:strokeWidth="0.1" | ||||
|         android:fillType="evenOdd"/> | ||||
|     <path | ||||
|         android:name="path_13" | ||||
|         android:pathData="M 56.182 67.511 C 56.182 67.511 56.633 66.248 59.585 63.634 C 59.585 63.634 61.253 61.991 59.985 58.114 C 59.985 58.114 59.642 56.06 61.789 57.255 C 61.789 57.255 66.239 60.46 62.98 65 C 62.98 65 62.351 66.117 63.761 65.78 L 76.066 63.103 C 76.066 63.103 77.386 62.752 77.641 64.163 C 77.641 64.163 77.812 65.372 76.746 65.672 L 69.85 67.378 C 69.85 67.378 72.829 69.597 69.85 70.825 C 69.85 70.825 72.404 72.779 69.446 74.268 C 69.446 74.268 71.387 76.015 68.516 77.019 C 68.516 77.019 64.633 78.474 61.317 77.831 C 57.618 77.103 54.057 72.114 56.182 67.511 Z" | ||||
|         android:fillColor="#f4900c" | ||||
|         android:strokeColor="#f4900c" | ||||
|         android:strokeLineCap="round" | ||||
|         android:strokeLineJoin="round" | ||||
|         android:strokeWidth="0.1" | ||||
|         android:fillType="evenOdd"/> | ||||
| </vector> | ||||
|   | ||||
							
								
								
									
										6
									
								
								app/src/debug/res/drawable/ic_migrate_direction.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| <vector android:height="100dp" | ||||
|     android:viewportHeight="24.0" android:viewportWidth="24.0" | ||||
|     android:width="100dp" xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:tint="?attr/colorControlNormal"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 -5,5z"/> | ||||
| </vector> | ||||
| @@ -1,6 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@drawable/ic_launcher_background"/> | ||||
|     <background android:drawable="@android:color/transparent"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
|     <monochrome android:drawable="@drawable/ic_launcher_monochrome"/> | ||||
| </adaptive-icon> | ||||
| </adaptive-icon> | ||||
| @@ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <background android:drawable="@android:color/transparent"/> | ||||
|     <foreground android:drawable="@drawable/ic_launcher_foreground"/> | ||||
| </adaptive-icon> | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										378
									
								
								app/src/main/AndroidManifest.xml
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						| @@ -1,118 +1,61 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools"> | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     package="eu.kanade.tachiyomi"> | ||||
|  | ||||
|     <!-- Internet --> | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | ||||
|  | ||||
|     <!-- Storage --> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.WRITE_EXTERNAL_STORAGE" | ||||
|         tools:ignore="ScopedStorage" /> | ||||
|  | ||||
|     <!-- For background jobs --> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK" /> | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> | ||||
|  | ||||
|     <!-- For managing extensions --> | ||||
|     <uses-permission android:name="android.permission.REQUEST_INSTALL_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+ --> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
|     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> | ||||
|     <uses-permission android:name="android.permission.USE_FINGERPRINT" /> | ||||
|     <uses-permission android:name="android.permission.GET_TASKS" /> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.QUERY_ALL_PACKAGES" | ||||
|         tools:ignore="QueryAllPackagesPermission" /> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | ||||
|     <uses-permission | ||||
|         android:name="android.permission.READ_APP_SPECIFIC_LOCALES" | ||||
|         android:name="android.permission.PACKAGE_USAGE_STATS" | ||||
|         tools:ignore="ProtectedPermissions" /> | ||||
|  | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | ||||
|     <!-- Lock vibrate --> | ||||
|     <uses-permission android:name="android.permission.VIBRATE" /> | ||||
|  | ||||
|     <application | ||||
|         android:name=".App" | ||||
|         android:allowBackup="false" | ||||
|         android:enableOnBackInvokedCallback="true" | ||||
|         android:allowBackup="true" | ||||
|         android:fullBackupContent="@xml/backup_rules" | ||||
|         android:hardwareAccelerated="true" | ||||
|         android:hasFragileUserData="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:largeHeap="true" | ||||
|         android:localeConfig="@xml/locales_config" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:preserveLegacyExternalStorage="true" | ||||
|         android:requestLegacyExternalStorage="true" | ||||
|         android:roundIcon="@mipmap/ic_launcher" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/Theme.Tachiyomi"> | ||||
|  | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:theme="@style/Theme.Tachiyomi.Light" | ||||
|         android:usesCleartextTraffic="true"> | ||||
|         <activity | ||||
|             android:name=".ui.main.MainActivity" | ||||
|             android:exported="true" | ||||
|             android:launchMode="singleTop" | ||||
|             android:theme="@style/Theme.Tachiyomi.SplashScreen"> | ||||
|             android:theme="@style/Theme.Splash"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Deep link to add repos --> | ||||
|             <intent-filter android:label="@string/action_add_repo"> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data android:scheme="tachiyomi" /> | ||||
|                 <data android:host="add-repo" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!-- Open backup files --> | ||||
|             <intent-filter android:label="@string/pref_restore_backup"> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data android:scheme="file" /> | ||||
|                 <data android:scheme="content" /> | ||||
|                 <data android:host="*" /> | ||||
|                 <data android:mimeType="*/*" /> | ||||
|                 <!-- | ||||
|                 Work around Android's ugly primitive PatternMatcher | ||||
|                 implementation that can't cope with finding a . early in | ||||
|                 the path unless it's explicitly matched. | ||||
|  | ||||
|                 See https://stackoverflow.com/a/31028507 | ||||
|                 --> | ||||
|                 <data android:pathPattern=".*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|                 <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <!--suppress AndroidDomInspection --> | ||||
|             <meta-data | ||||
|                 android:name="android.app.shortcuts" | ||||
|                 android:resource="@xml/shortcuts" /> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".crash.CrashActivity" | ||||
|             android:exported="false" | ||||
|             android:process=":error_handler" /> | ||||
|  | ||||
|             android:name=".ui.main.ForceCloseActivity" | ||||
|             android:clearTaskOnLaunch="true" | ||||
|             android:noHistory="true" | ||||
|             android:theme="@android:style/Theme.NoDisplay" /> | ||||
|         <activity | ||||
|             android:name=".ui.deeplink.DeepLinkActivity" | ||||
|             android:exported="true" | ||||
|             android:label="@string/action_search" | ||||
|             android:name=".ui.main.DeepLinkActivity" | ||||
|             android:launchMode="singleTask" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
| @@ -125,86 +68,77 @@ | ||||
|                 <action android:name="eu.kanade.tachiyomi.SEARCH" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEND" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <data android:mimeType="text/plain" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <meta-data | ||||
|                 android:name="android.app.searchable" | ||||
|                 android:resource="@xml/searchable" /> | ||||
|         </activity> | ||||
|  | ||||
|         <activity | ||||
|             android:name=".ui.reader.ReaderActivity" | ||||
|             android:exported="false" | ||||
|             android:launchMode="singleTask"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="com.samsung.android.support.REMOTE_ACTION" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <meta-data | ||||
|                 android:name="com.samsung.android.support.REMOTE_ACTION" | ||||
|                 android:resource="@xml/s_pen_actions" /> | ||||
|         </activity> | ||||
|  | ||||
|             android:launchMode="singleTask" /> | ||||
|         <activity | ||||
|             android:name=".ui.security.UnlockActivity" | ||||
|             android:exported="false" | ||||
|             android:theme="@style/Theme.Tachiyomi" /> | ||||
|  | ||||
|             android:name=".ui.security.BiometricUnlockActivity" | ||||
|             android:theme="@style/Theme.Splash" /> | ||||
|         <activity | ||||
|             android:name=".ui.webview.WebViewActivity" | ||||
|             android:configChanges="uiMode|orientation|screenSize" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|             android:configChanges="uiMode|orientation|screenSize" /> | ||||
|         <activity | ||||
|             android:name=".extension.util.ExtensionInstallActivity" | ||||
|             android:exported="false" | ||||
|             android:theme="@android:style/Theme.Translucent.NoTitleBar" /> | ||||
|  | ||||
|             android:name=".widget.CustomLayoutPickerActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:theme="@style/FilePickerTheme" /> | ||||
|         <activity | ||||
|             android:name=".ui.setting.track.TrackLoginActivity" | ||||
|             android:exported="true" | ||||
|             android:label="@string/track_activity_name"> | ||||
|             android:name=".ui.setting.track.AnilistLoginActivity" | ||||
|             android:label="Anilist"> | ||||
|             <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="mihon" /> | ||||
|                 <data | ||||
|                     android:host="anilist-auth" | ||||
|                     android:scheme="tachiyomi" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.track.ShikimoriLoginActivity" | ||||
|             android:label="Shikimori"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <data android:host="anilist-auth" /> | ||||
|                 <data android:host="bangumi-auth" /> | ||||
|                 <data android:host="myanimelist-auth" /> | ||||
|                 <data android:host="shikimori-auth" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <data | ||||
|                     android:host="shikimori-auth" | ||||
|                     android:scheme="tachiyomi" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.track.BangumiLoginActivity" | ||||
|             android:label="Bangumi"> | ||||
|             <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:host="bangumi-auth" | ||||
|                     android:scheme="tachiyomi" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".data.notification.NotificationReceiver" | ||||
|             android:exported="false" /> | ||||
|         <activity | ||||
|             android:name=".extension.util.ExtensionInstallActivity" | ||||
|             android:theme="@android:style/Theme.Translucent.NoTitleBar" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".extension.util.ExtensionInstallService" | ||||
|             android:exported="false" | ||||
|             android:foregroundServiceType="shortService" /> | ||||
|  | ||||
|         <service | ||||
|             android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" | ||||
|             android:enabled="false" | ||||
|             android:exported="false"> | ||||
|             <meta-data | ||||
|                 android:name="autoStoreLocales" | ||||
|                 android:value="true" /> | ||||
|         </service> | ||||
|  | ||||
|         <service | ||||
|             android:name="androidx.work.impl.foreground.SystemForegroundService" | ||||
|             android:foregroundServiceType="dataSync" | ||||
|             tools:node="merge" /> | ||||
|         <activity | ||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" | ||||
|             android:theme="@style/Theme.MaterialComponents" /> | ||||
|         <activity | ||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesActivity" | ||||
|             android:theme="@style/Theme.MaterialComponents" /> | ||||
|  | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
| @@ -216,21 +150,163 @@ | ||||
|                 android:resource="@xml/provider_paths" /> | ||||
|         </provider> | ||||
|  | ||||
|         <provider | ||||
|             android:name="rikka.shizuku.ShizukuProvider" | ||||
|             android:authorities="${applicationId}.shizuku" | ||||
|             android:enabled="true" | ||||
|             android:exported="true" | ||||
|             android:multiprocess="false" | ||||
|             android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" /> | ||||
|         <receiver | ||||
|             android:name=".data.notification.NotificationReceiver" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <meta-data | ||||
|             android:name="android.webkit.WebView.EnableSafeBrowsing" | ||||
|             android:value="false" /> | ||||
|         <meta-data | ||||
|             android:name="android.webkit.WebView.MetricsOptOut" | ||||
|             android:value="true" /> | ||||
|         <service | ||||
|             android:name=".data.library.LibraryUpdateService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.download.DownloadService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.updater.UpdaterService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.backup.BackupCreateService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.backup.BackupRestoreService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <!-- EH --> | ||||
|         <service | ||||
|             android:name="exh.eh.EHentaiUpdateWorker" | ||||
|             android:permission="android.permission.BIND_JOB_SERVICE" | ||||
|             android:exported="true" /> | ||||
|         <activity | ||||
|             android:name="exh.ui.intercept.InterceptActivity" | ||||
|             android:label="TachiyomiEH" | ||||
|             android:theme="@style/Theme.EHActivity"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
|  | ||||
|                 <!-- EH --> | ||||
|                 <data | ||||
|                     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="exhentai.org" | ||||
|                     android:pathPrefix="/g/" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="exhentai.org" | ||||
|                     android:pathPrefix="/g/" | ||||
|                     android:scheme="https" /> | ||||
|  | ||||
|                 <!-- nhentai --> | ||||
|                 <data | ||||
|                     android:host="nhentai.net" | ||||
|                     android:pathPrefix="/g/" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="nhentai.net" | ||||
|                     android:pathPrefix="/g/" | ||||
|                     android:scheme="https" /> | ||||
|  | ||||
|                 <!-- Perv Eden --> | ||||
|                 <data | ||||
|                     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:host="hentai.cafe" | ||||
|                     android:pathPattern="/.*/.*" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="hentai.cafe" | ||||
|                     android:pathPattern="/.*/.*" | ||||
|                     android:scheme="https" /> | ||||
|  | ||||
|                 <!-- Tsumino --> | ||||
|                 <data | ||||
|                     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:host="hitomi.la" | ||||
|                     android:pathPrefix="/galleries/" | ||||
|                     android:scheme="http" /> | ||||
|                 <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 --> | ||||
|                 <data | ||||
|                     android:host="pururin.io" | ||||
|                     android:pathPrefix="/gallery/" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="pururin.io" | ||||
|                     android:pathPrefix="/gallery/" | ||||
|                     android:scheme="https" /> | ||||
|  | ||||
|                 <!-- HBrowse --> | ||||
|                 <data | ||||
|                     android:host="www.hbrowse.com" | ||||
|                     android:pathPrefix="/" | ||||
|                     android:scheme="http" /> | ||||
|                 <data | ||||
|                     android:host="www.hbrowse.com" | ||||
|                     android:pathPrefix="/" | ||||
|                     android:scheme="https" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name="exh.ui.captcha.BrowserActionActivity" | ||||
|             android:theme="@style/Theme.EHActivity" /> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						| After Width: | Height: | Size: 22 KiB | 
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.core.preference | ||||
|  | ||||
| import androidx.compose.ui.state.ToggleableState | ||||
| import tachiyomi.core.common.preference.CheckboxState | ||||
|  | ||||
| fun <T> CheckboxState.TriState<T>.asToggleableState() = when (this) { | ||||
|     is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate | ||||
|     is CheckboxState.TriState.Include -> ToggleableState.On | ||||
|     is CheckboxState.TriState.None -> ToggleableState.Off | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package eu.kanade.core.preference | ||||
|  | ||||
| import androidx.compose.runtime.MutableState | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import tachiyomi.core.common.preference.Preference | ||||
|  | ||||
| class PreferenceMutableState<T>( | ||||
|     private val preference: Preference<T>, | ||||
|     scope: CoroutineScope, | ||||
| ) : MutableState<T> { | ||||
|  | ||||
|     private val state = mutableStateOf(preference.get()) | ||||
|  | ||||
|     init { | ||||
|         preference.changes() | ||||
|             .onEach { state.value = it } | ||||
|             .launchIn(scope) | ||||
|     } | ||||
|  | ||||
|     override var value: T | ||||
|         get() = state.value | ||||
|         set(value) { | ||||
|             preference.set(value) | ||||
|         } | ||||
|  | ||||
|     override fun component1(): T { | ||||
|         return state.value | ||||
|     } | ||||
|  | ||||
|     override fun component2(): (T) -> Unit { | ||||
|         return preference::set | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope) | ||||
| @@ -1,138 +0,0 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.ui.util.fastForEach | ||||
| import kotlin.contracts.ExperimentalContracts | ||||
| import kotlin.contracts.contract | ||||
|  | ||||
| fun <T : R, R : Any> List<T>.insertSeparators( | ||||
|     generator: (T?, T?) -> R?, | ||||
| ): List<R> { | ||||
|     if (isEmpty()) return emptyList() | ||||
|     val newList = mutableListOf<R>() | ||||
|     for (i in -1..lastIndex) { | ||||
|         val before = getOrNull(i) | ||||
|         before?.let(newList::add) | ||||
|         val after = getOrNull(i + 1) | ||||
|         val separator = generator.invoke(before, after) | ||||
|         separator?.let(newList::add) | ||||
|     } | ||||
|     return newList | ||||
| } | ||||
|  | ||||
| fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) { | ||||
|     if (shouldAdd) { | ||||
|         add(value) | ||||
|     } else { | ||||
|         remove(value) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only elements matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing all elements not matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val destination = ArrayList<T>() | ||||
|     fastForEach { if (!predicate(it)) destination.add(it) } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only the non-null results of applying the | ||||
|  * given [transform] function to each element in the original collection. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> { | ||||
|     contract { callsInPlace(transform) } | ||||
|     val destination = ArrayList<R>() | ||||
|     fastForEach { element -> | ||||
|         transform(element)?.let(destination::add) | ||||
|     } | ||||
|     return destination | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Splits the original collection into pair of lists, | ||||
|  * where *first* list contains elements for which [predicate] yielded `true`, | ||||
|  * while *second* list contains elements for which [predicate] yielded `false`. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     val first = ArrayList<T>() | ||||
|     val second = ArrayList<T>() | ||||
|     fastForEach { | ||||
|         if (predicate(it)) { | ||||
|             first.add(it) | ||||
|         } else { | ||||
|             second.add(it) | ||||
|         } | ||||
|     } | ||||
|     return Pair(first, second) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns the number of entries not matching the given [predicate]. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int { | ||||
|     contract { callsInPlace(predicate) } | ||||
|     var count = size | ||||
|     fastForEach { if (predicate(it)) --count } | ||||
|     return count | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns a list containing only elements from the given collection | ||||
|  * having distinct keys returned by the given [selector] function. | ||||
|  * | ||||
|  * Among elements of the given collection with equal keys, only the first one will be present in the resulting list. | ||||
|  * The elements in the resulting list are in the same order as they were in the source collection. | ||||
|  * | ||||
|  * **Do not use for collections that come from public APIs**, since they may not support random | ||||
|  * access in an efficient way, and this method may actually be a lot slower. Only use for | ||||
|  * collections that are created by code we control and are known to support random access. | ||||
|  */ | ||||
| @OptIn(ExperimentalContracts::class) | ||||
| inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> { | ||||
|     contract { callsInPlace(selector) } | ||||
|     val set = HashSet<K>() | ||||
|     val list = ArrayList<T>() | ||||
|     fastForEach { | ||||
|         val key = selector(it) | ||||
|         if (set.add(key)) list.add(it) | ||||
|     } | ||||
|     return list | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| package eu.kanade.core.util | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.remember | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| @Composable | ||||
| fun ifSourcesLoaded(): Boolean { | ||||
|     return remember { Injekt.get<SourceManager>().isInitialized }.collectAsState().value | ||||
| } | ||||
| @@ -1,195 +0,0 @@ | ||||
| package eu.kanade.domain | ||||
|  | ||||
| import eu.kanade.domain.chapter.interactor.GetAvailableScanlators | ||||
| import eu.kanade.domain.chapter.interactor.SetReadStatus | ||||
| import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource | ||||
| import eu.kanade.domain.download.interactor.DeleteDownload | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionLanguages | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionSources | ||||
| import eu.kanade.domain.extension.interactor.GetExtensionsByType | ||||
| import eu.kanade.domain.extension.interactor.TrustExtension | ||||
| import eu.kanade.domain.manga.interactor.GetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.SetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.SetMangaViewerFlags | ||||
| import eu.kanade.domain.manga.interactor.UpdateManga | ||||
| import eu.kanade.domain.source.interactor.GetEnabledSources | ||||
| import eu.kanade.domain.source.interactor.GetLanguagesWithSources | ||||
| import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.domain.source.interactor.ToggleLanguage | ||||
| import eu.kanade.domain.source.interactor.ToggleSource | ||||
| import eu.kanade.domain.source.interactor.ToggleSourcePin | ||||
| import eu.kanade.domain.track.interactor.AddTracks | ||||
| import eu.kanade.domain.track.interactor.RefreshTracks | ||||
| import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import mihon.data.repository.ExtensionRepoRepositoryImpl | ||||
| import mihon.domain.chapter.interactor.FilterChaptersForDownload | ||||
| import mihon.domain.extensionrepo.interactor.CreateExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.GetExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount | ||||
| import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo | ||||
| import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo | ||||
| import mihon.domain.extensionrepo.repository.ExtensionRepoRepository | ||||
| import mihon.domain.extensionrepo.service.ExtensionRepoService | ||||
| import mihon.domain.upcoming.interactor.GetUpcomingManga | ||||
| import tachiyomi.data.category.CategoryRepositoryImpl | ||||
| import tachiyomi.data.chapter.ChapterRepositoryImpl | ||||
| import tachiyomi.data.history.HistoryRepositoryImpl | ||||
| import tachiyomi.data.manga.MangaRepositoryImpl | ||||
| import tachiyomi.data.release.ReleaseServiceImpl | ||||
| import tachiyomi.data.source.SourceRepositoryImpl | ||||
| import tachiyomi.data.source.StubSourceRepositoryImpl | ||||
| import tachiyomi.data.track.TrackRepositoryImpl | ||||
| import tachiyomi.data.updates.UpdatesRepositoryImpl | ||||
| import tachiyomi.domain.category.interactor.CreateCategoryWithName | ||||
| import tachiyomi.domain.category.interactor.DeleteCategory | ||||
| import tachiyomi.domain.category.interactor.GetCategories | ||||
| import tachiyomi.domain.category.interactor.RenameCategory | ||||
| import tachiyomi.domain.category.interactor.ReorderCategory | ||||
| import tachiyomi.domain.category.interactor.ResetCategoryFlags | ||||
| import tachiyomi.domain.category.interactor.SetDisplayMode | ||||
| import tachiyomi.domain.category.interactor.SetMangaCategories | ||||
| import tachiyomi.domain.category.interactor.SetSortModeForCategory | ||||
| import tachiyomi.domain.category.interactor.UpdateCategory | ||||
| import tachiyomi.domain.category.repository.CategoryRepository | ||||
| import tachiyomi.domain.chapter.interactor.GetChapter | ||||
| import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags | ||||
| import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.history.interactor.GetHistory | ||||
| import tachiyomi.domain.history.interactor.GetNextChapters | ||||
| import tachiyomi.domain.history.interactor.GetTotalReadDuration | ||||
| import tachiyomi.domain.history.interactor.RemoveHistory | ||||
| import tachiyomi.domain.history.interactor.UpsertHistory | ||||
| import tachiyomi.domain.history.repository.HistoryRepository | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga | ||||
| import tachiyomi.domain.manga.interactor.GetFavorites | ||||
| import tachiyomi.domain.manga.interactor.GetLibraryManga | ||||
| import tachiyomi.domain.manga.interactor.GetManga | ||||
| import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId | ||||
| import tachiyomi.domain.manga.interactor.GetMangaWithChapters | ||||
| import tachiyomi.domain.manga.interactor.NetworkToLocalManga | ||||
| import tachiyomi.domain.manga.interactor.ResetViewerFlags | ||||
| import tachiyomi.domain.manga.interactor.SetMangaChapterFlags | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.domain.release.interactor.GetApplicationRelease | ||||
| import tachiyomi.domain.release.service.ReleaseService | ||||
| import tachiyomi.domain.source.interactor.GetRemoteManga | ||||
| import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.domain.source.repository.StubSourceRepository | ||||
| import tachiyomi.domain.track.interactor.DeleteTrack | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.GetTracksPerManga | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.domain.track.repository.TrackRepository | ||||
| import tachiyomi.domain.updates.interactor.GetUpdates | ||||
| import tachiyomi.domain.updates.repository.UpdatesRepository | ||||
| import uy.kohesive.injekt.api.InjektModule | ||||
| import uy.kohesive.injekt.api.InjektRegistrar | ||||
| import uy.kohesive.injekt.api.addFactory | ||||
| import uy.kohesive.injekt.api.addSingletonFactory | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class DomainModule : InjektModule { | ||||
|  | ||||
|     override fun InjektRegistrar.registerInjectables() { | ||||
|         addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) } | ||||
|         addFactory { GetCategories(get()) } | ||||
|         addFactory { ResetCategoryFlags(get(), get()) } | ||||
|         addFactory { SetDisplayMode(get()) } | ||||
|         addFactory { SetSortModeForCategory(get(), get()) } | ||||
|         addFactory { CreateCategoryWithName(get(), get()) } | ||||
|         addFactory { RenameCategory(get()) } | ||||
|         addFactory { ReorderCategory(get()) } | ||||
|         addFactory { UpdateCategory(get()) } | ||||
|         addFactory { DeleteCategory(get()) } | ||||
|  | ||||
|         addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } | ||||
|         addFactory { GetDuplicateLibraryManga(get()) } | ||||
|         addFactory { GetFavorites(get()) } | ||||
|         addFactory { GetLibraryManga(get()) } | ||||
|         addFactory { GetMangaWithChapters(get(), get()) } | ||||
|         addFactory { GetMangaByUrlAndSourceId(get()) } | ||||
|         addFactory { GetManga(get()) } | ||||
|         addFactory { GetNextChapters(get(), get(), get()) } | ||||
|         addFactory { GetUpcomingManga(get()) } | ||||
|         addFactory { ResetViewerFlags(get()) } | ||||
|         addFactory { SetMangaChapterFlags(get()) } | ||||
|         addFactory { FetchInterval(get()) } | ||||
|         addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) } | ||||
|         addFactory { SetMangaViewerFlags(get()) } | ||||
|         addFactory { NetworkToLocalManga(get()) } | ||||
|         addFactory { UpdateManga(get(), get()) } | ||||
|         addFactory { SetMangaCategories(get()) } | ||||
|         addFactory { GetExcludedScanlators(get()) } | ||||
|         addFactory { SetExcludedScanlators(get()) } | ||||
|  | ||||
|         addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) } | ||||
|         addFactory { GetApplicationRelease(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } | ||||
|         addFactory { TrackChapter(get(), get(), get(), get()) } | ||||
|         addFactory { AddTracks(get(), get(), get(), get()) } | ||||
|         addFactory { RefreshTracks(get(), get(), get(), get()) } | ||||
|         addFactory { DeleteTrack(get()) } | ||||
|         addFactory { GetTracksPerManga(get()) } | ||||
|         addFactory { GetTracks(get()) } | ||||
|         addFactory { InsertTrack(get()) } | ||||
|         addFactory { SyncChapterProgressWithTrack(get(), get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } | ||||
|         addFactory { GetChapter(get()) } | ||||
|         addFactory { GetChaptersByMangaId(get()) } | ||||
|         addFactory { GetChapterByUrlAndMangaId(get()) } | ||||
|         addFactory { UpdateChapter(get()) } | ||||
|         addFactory { SetReadStatus(get(), get(), get(), get()) } | ||||
|         addFactory { ShouldUpdateDbChapter() } | ||||
|         addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) } | ||||
|         addFactory { GetAvailableScanlators(get()) } | ||||
|         addFactory { FilterChaptersForDownload(get(), get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) } | ||||
|         addFactory { GetHistory(get()) } | ||||
|         addFactory { UpsertHistory(get()) } | ||||
|         addFactory { RemoveHistory(get()) } | ||||
|         addFactory { GetTotalReadDuration(get()) } | ||||
|  | ||||
|         addFactory { DeleteDownload(get(), get()) } | ||||
|  | ||||
|         addFactory { GetExtensionsByType(get(), get()) } | ||||
|         addFactory { GetExtensionSources(get()) } | ||||
|         addFactory { GetExtensionLanguages(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) } | ||||
|         addFactory { GetUpdates(get()) } | ||||
|  | ||||
|         addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) } | ||||
|         addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) } | ||||
|         addFactory { GetEnabledSources(get(), get()) } | ||||
|         addFactory { GetLanguagesWithSources(get(), get()) } | ||||
|         addFactory { GetRemoteManga(get()) } | ||||
|         addFactory { GetSourcesWithFavoriteCount(get(), get()) } | ||||
|         addFactory { GetSourcesWithNonLibraryManga(get()) } | ||||
|         addFactory { SetMigrateSorting(get()) } | ||||
|         addFactory { ToggleLanguage(get()) } | ||||
|         addFactory { ToggleSource(get()) } | ||||
|         addFactory { ToggleSourcePin(get()) } | ||||
|         addFactory { TrustExtension(get(), get()) } | ||||
|  | ||||
|         addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) } | ||||
|         addFactory { ExtensionRepoService(get(), get()) } | ||||
|         addFactory { GetExtensionRepo(get()) } | ||||
|         addFactory { GetExtensionRepoCount(get()) } | ||||
|         addFactory { CreateExtensionRepo(get(), get()) } | ||||
|         addFactory { DeleteExtensionRepo(get()) } | ||||
|         addFactory { ReplaceExtensionRepo(get()) } | ||||
|         addFactory { UpdateExtensionRepo(get(), get()) } | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| package eu.kanade.domain.base | ||||
|  | ||||
| import android.content.Context | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| class BasePreferences( | ||||
|     val context: Context, | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun downloadedOnly() = preferenceStore.getBoolean( | ||||
|         Preference.appStateKey("pref_downloaded_only"), | ||||
|         false, | ||||
|     ) | ||||
|  | ||||
|     fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false) | ||||
|  | ||||
|     fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore) | ||||
|  | ||||
|     fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false) | ||||
|  | ||||
|     enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) { | ||||
|         LEGACY(MR.strings.ext_installer_legacy, true), | ||||
|         PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true), | ||||
|         SHIZUKU(MR.strings.ext_installer_shizuku, false), | ||||
|         PRIVATE(MR.strings.ext_installer_private, false), | ||||
|     } | ||||
|  | ||||
|     fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "") | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package eu.kanade.domain.base | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.domain.base.BasePreferences.ExtensionInstaller | ||||
| import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller | ||||
| import eu.kanade.tachiyomi.util.system.isShizukuInstalled | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
|  | ||||
| class ExtensionInstallerPreference( | ||||
|     private val context: Context, | ||||
|     preferenceStore: PreferenceStore, | ||||
| ) : Preference<ExtensionInstaller> { | ||||
|  | ||||
|     private val basePref = preferenceStore.getEnum(key(), defaultValue()) | ||||
|  | ||||
|     override fun key() = "extension_installer" | ||||
|  | ||||
|     val entries get() = ExtensionInstaller.entries.run { | ||||
|         if (context.hasMiuiPackageInstaller) { | ||||
|             filter { it != ExtensionInstaller.PACKAGEINSTALLER } | ||||
|         } else { | ||||
|             toList() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun defaultValue() = if (context.hasMiuiPackageInstaller) { | ||||
|         ExtensionInstaller.LEGACY | ||||
|     } else { | ||||
|         ExtensionInstaller.PACKAGEINSTALLER | ||||
|     } | ||||
|  | ||||
|     private fun check(value: ExtensionInstaller): ExtensionInstaller { | ||||
|         when (value) { | ||||
|             ExtensionInstaller.PACKAGEINSTALLER -> { | ||||
|                 if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY | ||||
|             } | ||||
|             ExtensionInstaller.SHIZUKU -> { | ||||
|                 if (!context.isShizukuInstalled) return defaultValue() | ||||
|             } | ||||
|             else -> {} | ||||
|         } | ||||
|         return value | ||||
|     } | ||||
|  | ||||
|     override fun get(): ExtensionInstaller { | ||||
|         val value = basePref.get() | ||||
|         val checkedValue = check(value) | ||||
|         if (value != checkedValue) { | ||||
|             basePref.set(checkedValue) | ||||
|         } | ||||
|         return checkedValue | ||||
|     } | ||||
|  | ||||
|     override fun set(value: ExtensionInstaller) { | ||||
|         basePref.set(check(value)) | ||||
|     } | ||||
|  | ||||
|     override fun isSet() = basePref.isSet() | ||||
|  | ||||
|     override fun delete() = basePref.delete() | ||||
|  | ||||
|     override fun changes() = basePref.changes() | ||||
|  | ||||
|     override fun stateIn(scope: CoroutineScope) = basePref.stateIn(scope) | ||||
| } | ||||
| @@ -1,24 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
|  | ||||
| class GetAvailableScanlators( | ||||
|     private val repository: ChapterRepository, | ||||
| ) { | ||||
|  | ||||
|     private fun List<String>.cleanupAvailableScanlators(): Set<String> { | ||||
|         return mapNotNull { it.ifBlank { null } }.toSet() | ||||
|     } | ||||
|  | ||||
|     suspend fun await(mangaId: Long): Set<String> { | ||||
|         return repository.getScanlatorsByMangaId(mangaId) | ||||
|             .cleanupAvailableScanlators() | ||||
|     } | ||||
|  | ||||
|     fun subscribe(mangaId: Long): Flow<Set<String>> { | ||||
|         return repository.getScanlatorsByMangaIdAsFlow(mangaId) | ||||
|             .map { it.cleanupAvailableScanlators() } | ||||
|     } | ||||
| } | ||||
| @@ -1,80 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import eu.kanade.domain.download.interactor.DeleteDownload | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.ChapterUpdate | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.download.service.DownloadPreferences | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
|  | ||||
| class SetReadStatus( | ||||
|     private val downloadPreferences: DownloadPreferences, | ||||
|     private val deleteDownload: DeleteDownload, | ||||
|     private val mangaRepository: MangaRepository, | ||||
|     private val chapterRepository: ChapterRepository, | ||||
| ) { | ||||
|  | ||||
|     private val mapper = { chapter: Chapter, read: Boolean -> | ||||
|         ChapterUpdate( | ||||
|             read = read, | ||||
|             lastPageRead = if (!read) 0 else null, | ||||
|             id = chapter.id, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext { | ||||
|         val chaptersToUpdate = chapters.filter { | ||||
|             when (read) { | ||||
|                 true -> !it.read | ||||
|                 false -> it.read || it.lastPageRead > 0 | ||||
|             } | ||||
|         } | ||||
|         if (chaptersToUpdate.isEmpty()) { | ||||
|             return@withNonCancellableContext Result.NoChapters | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             chapterRepository.updateAll( | ||||
|                 chaptersToUpdate.map { mapper(it, read) }, | ||||
|             ) | ||||
|         } catch (e: Exception) { | ||||
|             logcat(LogPriority.ERROR, e) | ||||
|             return@withNonCancellableContext Result.InternalError(e) | ||||
|         } | ||||
|  | ||||
|         if (read && downloadPreferences.removeAfterMarkedAsRead().get()) { | ||||
|             chaptersToUpdate | ||||
|                 .groupBy { it.mangaId } | ||||
|                 .forEach { (mangaId, chapters) -> | ||||
|                     deleteDownload.awaitAll( | ||||
|                         manga = mangaRepository.getMangaById(mangaId), | ||||
|                         chapters = chapters.toTypedArray(), | ||||
|                     ) | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         Result.Success | ||||
|     } | ||||
|  | ||||
|     suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext { | ||||
|         await( | ||||
|             read = read, | ||||
|             chapters = chapterRepository | ||||
|                 .getChapterByMangaId(mangaId) | ||||
|                 .toTypedArray(), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun await(manga: Manga, read: Boolean) = | ||||
|         await(manga.id, read) | ||||
|  | ||||
|     sealed interface Result { | ||||
|         data object Success : Result | ||||
|         data object NoChapters : Result | ||||
|         data class InternalError(val error: Throwable) : Result | ||||
|     } | ||||
| } | ||||
| @@ -1,213 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.interactor | ||||
|  | ||||
| import eu.kanade.domain.chapter.model.copyFromSChapter | ||||
| import eu.kanade.domain.chapter.model.toSChapter | ||||
| import eu.kanade.domain.manga.interactor.GetExcludedScanlators | ||||
| import eu.kanade.domain.manga.interactor.UpdateManga | ||||
| import eu.kanade.domain.manga.model.toSManga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadProvider | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import tachiyomi.data.chapter.ChapterSanitizer | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.model.NoChaptersException | ||||
| import tachiyomi.domain.chapter.model.toChapterUpdate | ||||
| import tachiyomi.domain.chapter.repository.ChapterRepository | ||||
| import tachiyomi.domain.chapter.service.ChapterRecognition | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.lang.Long.max | ||||
| import java.time.ZonedDateTime | ||||
| import java.util.TreeSet | ||||
|  | ||||
| class SyncChaptersWithSource( | ||||
|     private val downloadManager: DownloadManager, | ||||
|     private val downloadProvider: DownloadProvider, | ||||
|     private val chapterRepository: ChapterRepository, | ||||
|     private val shouldUpdateDbChapter: ShouldUpdateDbChapter, | ||||
|     private val updateManga: UpdateManga, | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
|     private val getExcludedScanlators: GetExcludedScanlators, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Method to synchronize db chapters with source ones | ||||
|      * | ||||
|      * @param rawSourceChapters the chapters from the source. | ||||
|      * @param manga the manga the chapters belong to. | ||||
|      * @param source the source the manga belongs to. | ||||
|      * @return Newly added chapters | ||||
|      */ | ||||
|     suspend fun await( | ||||
|         rawSourceChapters: List<SChapter>, | ||||
|         manga: Manga, | ||||
|         source: Source, | ||||
|         manualFetch: Boolean = false, | ||||
|         fetchWindow: Pair<Long, Long> = Pair(0, 0), | ||||
|     ): List<Chapter> { | ||||
|         if (rawSourceChapters.isEmpty() && !source.isLocal()) { | ||||
|             throw NoChaptersException() | ||||
|         } | ||||
|  | ||||
|         val now = ZonedDateTime.now() | ||||
|         val nowMillis = now.toInstant().toEpochMilli() | ||||
|  | ||||
|         val sourceChapters = rawSourceChapters | ||||
|             .distinctBy { it.url } | ||||
|             .mapIndexed { i, sChapter -> | ||||
|                 Chapter.create() | ||||
|                     .copyFromSChapter(sChapter) | ||||
|                     .copy(name = with(ChapterSanitizer) { sChapter.name.sanitize(manga.title) }) | ||||
|                     .copy(mangaId = manga.id, sourceOrder = i.toLong()) | ||||
|             } | ||||
|  | ||||
|         val dbChapters = getChaptersByMangaId.await(manga.id) | ||||
|  | ||||
|         val newChapters = mutableListOf<Chapter>() | ||||
|         val updatedChapters = mutableListOf<Chapter>() | ||||
|         val removedChapters = dbChapters.filterNot { dbChapter -> | ||||
|             sourceChapters.any { sourceChapter -> | ||||
|                 dbChapter.url == sourceChapter.url | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Used to not set upload date of older chapters | ||||
|         // to a higher value than newer chapters | ||||
|         var maxSeenUploadDate = 0L | ||||
|  | ||||
|         for (sourceChapter in sourceChapters) { | ||||
|             var chapter = sourceChapter | ||||
|  | ||||
|             // Update metadata from source if necessary. | ||||
|             if (source is HttpSource) { | ||||
|                 val sChapter = chapter.toSChapter() | ||||
|                 source.prepareNewChapter(sChapter, manga.toSManga()) | ||||
|                 chapter = chapter.copyFromSChapter(sChapter) | ||||
|             } | ||||
|  | ||||
|             // Recognize chapter number for the chapter. | ||||
|             val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber) | ||||
|             chapter = chapter.copy(chapterNumber = chapterNumber) | ||||
|  | ||||
|             val dbChapter = dbChapters.find { it.url == chapter.url } | ||||
|  | ||||
|             if (dbChapter == null) { | ||||
|                 val toAddChapter = if (chapter.dateUpload == 0L) { | ||||
|                     val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate | ||||
|                     chapter.copy(dateUpload = altDateUpload) | ||||
|                 } else { | ||||
|                     maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload) | ||||
|                     chapter | ||||
|                 } | ||||
|                 newChapters.add(toAddChapter) | ||||
|             } else { | ||||
|                 if (shouldUpdateDbChapter.await(dbChapter, chapter)) { | ||||
|                     val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) && | ||||
|                         downloadManager.isChapterDownloaded( | ||||
|                             dbChapter.name, | ||||
|                             dbChapter.scanlator, | ||||
|                             manga.title, | ||||
|                             manga.source, | ||||
|                         ) | ||||
|  | ||||
|                     if (shouldRenameChapter) { | ||||
|                         downloadManager.renameChapter(source, manga, dbChapter, chapter) | ||||
|                     } | ||||
|                     var toChangeChapter = dbChapter.copy( | ||||
|                         name = chapter.name, | ||||
|                         chapterNumber = chapter.chapterNumber, | ||||
|                         scanlator = chapter.scanlator, | ||||
|                         sourceOrder = chapter.sourceOrder, | ||||
|                     ) | ||||
|                     if (chapter.dateUpload != 0L) { | ||||
|                         toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload) | ||||
|                     } | ||||
|                     updatedChapters.add(toChangeChapter) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Return if there's nothing to add, delete, or update to avoid unnecessary db transactions. | ||||
|         if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) { | ||||
|             if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) { | ||||
|                 updateManga.awaitUpdateFetchInterval( | ||||
|                     manga, | ||||
|                     now, | ||||
|                     fetchWindow, | ||||
|                 ) | ||||
|             } | ||||
|             return emptyList() | ||||
|         } | ||||
|  | ||||
|         val reAdded = mutableListOf<Chapter>() | ||||
|  | ||||
|         val deletedChapterNumbers = TreeSet<Double>() | ||||
|         val deletedReadChapterNumbers = TreeSet<Double>() | ||||
|         val deletedBookmarkedChapterNumbers = TreeSet<Double>() | ||||
|  | ||||
|         removedChapters.forEach { chapter -> | ||||
|             if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber) | ||||
|             if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber) | ||||
|             deletedChapterNumbers.add(chapter.chapterNumber) | ||||
|         } | ||||
|  | ||||
|         val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch } | ||||
|             .associate { it.chapterNumber to it.dateFetch } | ||||
|  | ||||
|         // Date fetch is set in such a way that the upper ones will have bigger value than the lower ones | ||||
|         // Sources MUST return the chapters from most to less recent, which is common. | ||||
|         var itemCount = newChapters.size | ||||
|         var updatedToAdd = newChapters.map { toAddItem -> | ||||
|             var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--) | ||||
|  | ||||
|             if (!chapter.isRecognizedNumber || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter | ||||
|  | ||||
|             chapter = chapter.copy( | ||||
|                 read = chapter.chapterNumber in deletedReadChapterNumbers, | ||||
|                 bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers, | ||||
|             ) | ||||
|  | ||||
|             // Try to to use the fetch date of the original entry to not pollute 'Updates' tab | ||||
|             deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let { | ||||
|                 chapter = chapter.copy(dateFetch = it) | ||||
|             } | ||||
|  | ||||
|             reAdded.add(chapter) | ||||
|  | ||||
|             chapter | ||||
|         } | ||||
|  | ||||
|         if (removedChapters.isNotEmpty()) { | ||||
|             val toDeleteIds = removedChapters.map { it.id } | ||||
|             chapterRepository.removeChaptersWithIds(toDeleteIds) | ||||
|         } | ||||
|  | ||||
|         if (updatedToAdd.isNotEmpty()) { | ||||
|             updatedToAdd = chapterRepository.addAll(updatedToAdd) | ||||
|         } | ||||
|  | ||||
|         if (updatedChapters.isNotEmpty()) { | ||||
|             val chapterUpdates = updatedChapters.map { it.toChapterUpdate() } | ||||
|             updateChapter.awaitAll(chapterUpdates) | ||||
|         } | ||||
|         updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow) | ||||
|  | ||||
|         // Set this manga as updated since chapters were changed | ||||
|         // Note that last_update actually represents last time the chapter list changed at all | ||||
|         updateManga.awaitUpdateLastUpdate(manga.id) | ||||
|  | ||||
|         val reAddedUrls = reAdded.map { it.url }.toHashSet() | ||||
|  | ||||
|         val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet() | ||||
|  | ||||
|         return updatedToAdd.filterNot { | ||||
|             it.url in reAddedUrls || it.scanlator in excludedScanlators | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,42 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.model | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.ChapterImpl | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter | ||||
|  | ||||
| // TODO: Remove when all deps are migrated | ||||
| fun Chapter.toSChapter(): SChapter { | ||||
|     return SChapter.create().also { | ||||
|         it.url = url | ||||
|         it.name = name | ||||
|         it.date_upload = dateUpload | ||||
|         it.chapter_number = chapterNumber.toFloat() | ||||
|         it.scanlator = scanlator | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter { | ||||
|     return this.copy( | ||||
|         name = sChapter.name, | ||||
|         url = sChapter.url, | ||||
|         dateUpload = sChapter.date_upload, | ||||
|         chapterNumber = sChapter.chapter_number.toDouble(), | ||||
|         scanlator = sChapter.scanlator?.ifBlank { null }?.trim(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also { | ||||
|     it.id = id | ||||
|     it.manga_id = mangaId | ||||
|     it.url = url | ||||
|     it.name = name | ||||
|     it.scanlator = scanlator | ||||
|     it.read = read | ||||
|     it.bookmark = bookmark | ||||
|     it.last_page_read = lastPageRead.toInt() | ||||
|     it.date_fetch = dateFetch | ||||
|     it.date_upload = dateUpload | ||||
|     it.chapter_number = chapterNumber.toFloat() | ||||
|     it.source_order = sourceOrder.toInt() | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| package eu.kanade.domain.chapter.model | ||||
|  | ||||
| import eu.kanade.domain.manga.model.downloadedFilter | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.ui.manga.ChapterList | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.chapter.service.getChapterSort | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.applyFilter | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| /** | ||||
|  * Applies the view filters to the list of chapters obtained from the database. | ||||
|  * @return an observable of the list of chapters filtered and sorted. | ||||
|  */ | ||||
| fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> { | ||||
|     val isLocalManga = manga.isLocal() | ||||
|     val unreadFilter = manga.unreadFilter | ||||
|     val downloadedFilter = manga.downloadedFilter | ||||
|     val bookmarkedFilter = manga.bookmarkedFilter | ||||
|  | ||||
|     return filter { chapter -> applyFilter(unreadFilter) { !chapter.read } } | ||||
|         .filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } } | ||||
|         .filter { chapter -> | ||||
|             applyFilter(downloadedFilter) { | ||||
|                 val downloaded = downloadManager.isChapterDownloaded( | ||||
|                     chapter.name, | ||||
|                     chapter.scanlator, | ||||
|                     manga.title, | ||||
|                     manga.source, | ||||
|                 ) | ||||
|                 downloaded || isLocalManga | ||||
|             } | ||||
|         } | ||||
|         .sortedWith(getChapterSort(manga)) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Applies the view filters to the list of chapters obtained from the database. | ||||
|  * @return an observable of the list of chapters filtered and sorted. | ||||
|  */ | ||||
| fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> { | ||||
|     val isLocalManga = manga.isLocal() | ||||
|     val unreadFilter = manga.unreadFilter | ||||
|     val downloadedFilter = manga.downloadedFilter | ||||
|     val bookmarkedFilter = manga.bookmarkedFilter | ||||
|     return asSequence() | ||||
|         .filter { (chapter) -> applyFilter(unreadFilter) { !chapter.read } } | ||||
|         .filter { (chapter) -> applyFilter(bookmarkedFilter) { chapter.bookmark } } | ||||
|         .filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } } | ||||
|         .sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) } | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package eu.kanade.domain.download.interactor | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.service.SourceManager | ||||
|  | ||||
| class DeleteDownload( | ||||
|     private val sourceManager: SourceManager, | ||||
|     private val downloadManager: DownloadManager, | ||||
| ) { | ||||
|  | ||||
|     suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext { | ||||
|         sourceManager.get(manga.source)?.let { source -> | ||||
|             downloadManager.deleteChapters(chapters.toList(), manga, source) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
|  | ||||
| class GetExtensionLanguages( | ||||
|     private val preferences: SourcePreferences, | ||||
|     private val extensionManager: ExtensionManager, | ||||
| ) { | ||||
|     fun subscribe(): Flow<List<String>> { | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             extensionManager.availableExtensionsFlow, | ||||
|         ) { enabledLanguage, availableExtensions -> | ||||
|             availableExtensions | ||||
|                 .flatMap { ext -> | ||||
|                     if (ext.sources.isEmpty()) { | ||||
|                         listOf(ext.lang) | ||||
|                     } else { | ||||
|                         ext.sources.map { it.lang } | ||||
|                     } | ||||
|                 } | ||||
|                 .distinct() | ||||
|                 .sortedWith( | ||||
|                     compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
|  | ||||
| class GetExtensionSources( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> { | ||||
|         val isMultiSource = extension.sources.size > 1 | ||||
|         val isMultiLangSingleSource = | ||||
|             isMultiSource && extension.sources.map { it.name }.distinct().size == 1 | ||||
|  | ||||
|         return preferences.disabledSources().changes().map { disabledSources -> | ||||
|             fun Source.isEnabled() = id.toString() !in disabledSources | ||||
|  | ||||
|             extension.sources | ||||
|                 .map { source -> | ||||
|                     ExtensionSourceItem( | ||||
|                         source = source, | ||||
|                         enabled = source.isEnabled(), | ||||
|                         labelAsName = isMultiSource && !isMultiLangSingleSource, | ||||
|                     ) | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| data class ExtensionSourceItem( | ||||
|     val source: Source, | ||||
|     val enabled: Boolean, | ||||
|     val labelAsName: Boolean, | ||||
| ) | ||||
| @@ -1,60 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import eu.kanade.domain.extension.model.Extensions | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
|  | ||||
| class GetExtensionsByType( | ||||
|     private val preferences: SourcePreferences, | ||||
|     private val extensionManager: ExtensionManager, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<Extensions> { | ||||
|         val showNsfwSources = preferences.showNsfwSource().get() | ||||
|  | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             extensionManager.installedExtensionsFlow, | ||||
|             extensionManager.untrustedExtensionsFlow, | ||||
|             extensionManager.availableExtensionsFlow, | ||||
|         ) { enabledLanguages, _installed, _untrusted, _available -> | ||||
|             val (updates, installed) = _installed | ||||
|                 .filter { (showNsfwSources || !it.isNsfw) } | ||||
|                 .sortedWith( | ||||
|                     compareBy<Extension.Installed> { !it.isObsolete } | ||||
|                         .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, | ||||
|                 ) | ||||
|                 .partition { it.hasUpdate } | ||||
|  | ||||
|             val untrusted = _untrusted | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|  | ||||
|             val available = _available | ||||
|                 .filter { extension -> | ||||
|                     _installed.none { it.pkgName == extension.pkgName } && | ||||
|                         _untrusted.none { it.pkgName == extension.pkgName } && | ||||
|                         (showNsfwSources || !extension.isNsfw) | ||||
|                 } | ||||
|                 .flatMap { ext -> | ||||
|                     if (ext.sources.isEmpty()) { | ||||
|                         return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() | ||||
|                     } | ||||
|                     ext.sources.filter { it.lang in enabledLanguages } | ||||
|                         .map { | ||||
|                             ext.copy( | ||||
|                                 name = it.name, | ||||
|                                 lang = it.lang, | ||||
|                                 pkgName = "${ext.pkgName}-${it.id}", | ||||
|                                 sources = listOf(it), | ||||
|                             ) | ||||
|                         } | ||||
|                 } | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|  | ||||
|             Extensions(updates, installed, available, untrusted) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| package eu.kanade.domain.extension.interactor | ||||
|  | ||||
| import android.content.pm.PackageInfo | ||||
| import androidx.core.content.pm.PackageInfoCompat | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import mihon.domain.extensionrepo.repository.ExtensionRepoRepository | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
|  | ||||
| class TrustExtension( | ||||
|     private val extensionRepoRepository: ExtensionRepoRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean { | ||||
|         val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() | ||||
|         val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" | ||||
|         return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get() | ||||
|     } | ||||
|  | ||||
|     fun trust(pkgName: String, versionCode: Long, signatureHash: String) { | ||||
|         preferences.trustedExtensions().getAndSet { exts -> | ||||
|             // Remove previously trusted versions | ||||
|             val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet() | ||||
|  | ||||
|             removed.also { it += "$pkgName:$versionCode:$signatureHash" } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun revokeAll() { | ||||
|         preferences.trustedExtensions().delete() | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package eu.kanade.domain.extension.model | ||||
|  | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
|  | ||||
| data class Extensions( | ||||
|     val updates: List<Extension.Installed>, | ||||
|     val installed: List<Extension.Installed>, | ||||
|     val available: List<Extension.Available>, | ||||
|     val untrusted: List<Extension.Untrusted>, | ||||
| ) | ||||
| @@ -1,24 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.map | ||||
| import tachiyomi.data.DatabaseHandler | ||||
|  | ||||
| class GetExcludedScanlators( | ||||
|     private val handler: DatabaseHandler, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaId: Long): Set<String> { | ||||
|         return handler.awaitList { | ||||
|             excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|         } | ||||
|             .toSet() | ||||
|     } | ||||
|  | ||||
|     fun subscribe(mangaId: Long): Flow<Set<String>> { | ||||
|         return handler.subscribeToList { | ||||
|             excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|         } | ||||
|             .map { it.toSet() } | ||||
|     } | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import tachiyomi.data.DatabaseHandler | ||||
|  | ||||
| class SetExcludedScanlators( | ||||
|     private val handler: DatabaseHandler, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaId: Long, excludedScanlators: Set<String>) { | ||||
|         handler.await(inTransaction = true) { | ||||
|             val currentExcluded = handler.awaitList { | ||||
|                 excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId) | ||||
|             }.toSet() | ||||
|             val toAdd = excludedScanlators.minus(currentExcluded) | ||||
|             for (scanlator in toAdd) { | ||||
|                 excluded_scanlatorsQueries.insert(mangaId, scanlator) | ||||
|             } | ||||
|             val toRemove = currentExcluded.minus(excludedScanlators) | ||||
|             excluded_scanlatorsQueries.remove(mangaId, toRemove) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
|  | ||||
| class SetMangaViewerFlags( | ||||
|     private val mangaRepository: MangaRepository, | ||||
| ) { | ||||
|  | ||||
|     suspend fun awaitSetReadingMode(id: Long, flag: Long) { | ||||
|         val manga = mangaRepository.getMangaById(id) | ||||
|         mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = id, | ||||
|                 viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitSetOrientation(id: Long, flag: Long) { | ||||
|         val manga = mangaRepository.getMangaById(id) | ||||
|         mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = id, | ||||
|                 viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()), | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private fun Long.setFlag(flag: Long, mask: Long): Long { | ||||
|         return this and mask.inv() or (flag and mask) | ||||
|     } | ||||
| } | ||||
| @@ -1,106 +0,0 @@ | ||||
| package eu.kanade.domain.manga.interactor | ||||
|  | ||||
| import eu.kanade.domain.manga.model.hasCustomCover | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import tachiyomi.domain.manga.interactor.FetchInterval | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.manga.model.MangaUpdate | ||||
| import tachiyomi.domain.manga.repository.MangaRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.Instant | ||||
| import java.time.ZonedDateTime | ||||
|  | ||||
| class UpdateManga( | ||||
|     private val mangaRepository: MangaRepository, | ||||
|     private val fetchInterval: FetchInterval, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(mangaUpdate: MangaUpdate): Boolean { | ||||
|         return mangaRepository.update(mangaUpdate) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitAll(mangaUpdates: List<MangaUpdate>): Boolean { | ||||
|         return mangaRepository.updateAll(mangaUpdates) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFromSource( | ||||
|         localManga: Manga, | ||||
|         remoteManga: SManga, | ||||
|         manualFetch: Boolean, | ||||
|         coverCache: CoverCache = Injekt.get(), | ||||
|     ): Boolean { | ||||
|         val remoteTitle = try { | ||||
|             remoteManga.title | ||||
|         } catch (_: UninitializedPropertyAccessException) { | ||||
|             "" | ||||
|         } | ||||
|  | ||||
|         // if the manga isn't a favorite, set its title from source and update in db | ||||
|         val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle | ||||
|  | ||||
|         val coverLastModified = | ||||
|             when { | ||||
|                 // Never refresh covers if the url is empty to avoid "losing" existing covers | ||||
|                 remoteManga.thumbnail_url.isNullOrEmpty() -> null | ||||
|                 !manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null | ||||
|                 localManga.isLocal() -> Instant.now().toEpochMilli() | ||||
|                 localManga.hasCustomCover(coverCache) -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     null | ||||
|                 } | ||||
|                 else -> { | ||||
|                     coverCache.deleteFromCache(localManga, false) | ||||
|                     Instant.now().toEpochMilli() | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|         val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() } | ||||
|  | ||||
|         return mangaRepository.update( | ||||
|             MangaUpdate( | ||||
|                 id = localManga.id, | ||||
|                 title = title, | ||||
|                 coverLastModified = coverLastModified, | ||||
|                 author = remoteManga.author, | ||||
|                 artist = remoteManga.artist, | ||||
|                 description = remoteManga.description, | ||||
|                 genre = remoteManga.getGenres(), | ||||
|                 thumbnailUrl = thumbnailUrl, | ||||
|                 status = remoteManga.status.toLong(), | ||||
|                 updateStrategy = remoteManga.update_strategy, | ||||
|                 initialized = true, | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFetchInterval( | ||||
|         manga: Manga, | ||||
|         dateTime: ZonedDateTime = ZonedDateTime.now(), | ||||
|         window: Pair<Long, Long> = fetchInterval.getWindow(dateTime), | ||||
|     ): Boolean { | ||||
|         return mangaRepository.update( | ||||
|             fetchInterval.toMangaUpdate(manga, dateTime, window), | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean { | ||||
|         return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli())) | ||||
|     } | ||||
|  | ||||
|     suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean { | ||||
|         val dateAdded = when (favorite) { | ||||
|             true -> Instant.now().toEpochMilli() | ||||
|             false -> 0 | ||||
|         } | ||||
|         return mangaRepository.update( | ||||
|             MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| package eu.kanade.domain.manga.model | ||||
|  | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation | ||||
| import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode | ||||
| import tachiyomi.core.common.preference.TriState | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfo | ||||
| import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus | ||||
| import tachiyomi.domain.chapter.model.Chapter | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| // TODO: move these into the domain model | ||||
| val Manga.readingMode: Long | ||||
|     get() = viewerFlags and ReadingMode.MASK.toLong() | ||||
|  | ||||
| val Manga.readerOrientation: Long | ||||
|     get() = viewerFlags and ReaderOrientation.MASK.toLong() | ||||
|  | ||||
| val Manga.downloadedFilter: TriState | ||||
|     get() { | ||||
|         if (forceDownloaded()) return TriState.ENABLED_IS | ||||
|         return when (downloadedFilterRaw) { | ||||
|             Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS | ||||
|             Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT | ||||
|             else -> TriState.DISABLED | ||||
|         } | ||||
|     } | ||||
| fun Manga.chaptersFiltered(): Boolean { | ||||
|     return unreadFilter != TriState.DISABLED || | ||||
|         downloadedFilter != TriState.DISABLED || | ||||
|         bookmarkedFilter != TriState.DISABLED | ||||
| } | ||||
| fun Manga.forceDownloaded(): Boolean { | ||||
|     return favorite && Injekt.get<BasePreferences>().downloadedOnly().get() | ||||
| } | ||||
|  | ||||
| fun Manga.toSManga(): SManga = SManga.create().also { | ||||
|     it.url = url | ||||
|     it.title = title | ||||
|     it.artist = artist | ||||
|     it.author = author | ||||
|     it.description = description | ||||
|     it.genre = genre.orEmpty().joinToString() | ||||
|     it.status = status.toInt() | ||||
|     it.thumbnail_url = thumbnailUrl | ||||
|     it.initialized = initialized | ||||
| } | ||||
|  | ||||
| fun Manga.copyFrom(other: SManga): Manga { | ||||
|     val author = other.author ?: author | ||||
|     val artist = other.artist ?: artist | ||||
|     val description = other.description ?: description | ||||
|     val genres = if (other.genre != null) { | ||||
|         other.getGenres() | ||||
|     } else { | ||||
|         genre | ||||
|     } | ||||
|     val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl | ||||
|     return this.copy( | ||||
|         author = author, | ||||
|         artist = artist, | ||||
|         description = description, | ||||
|         genre = genres, | ||||
|         thumbnailUrl = thumbnailUrl, | ||||
|         status = other.status.toLong(), | ||||
|         updateStrategy = other.update_strategy, | ||||
|         initialized = other.initialized && initialized, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun SManga.toDomainManga(sourceId: Long): Manga { | ||||
|     return Manga.create().copy( | ||||
|         url = url, | ||||
|         title = title, | ||||
|         artist = artist, | ||||
|         author = author, | ||||
|         description = description, | ||||
|         genre = getGenres(), | ||||
|         status = status.toLong(), | ||||
|         thumbnailUrl = thumbnail_url, | ||||
|         updateStrategy = update_strategy, | ||||
|         initialized = initialized, | ||||
|         source = sourceId, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean { | ||||
|     return coverCache.getCustomCoverFile(id).exists() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates a ComicInfo instance based on the manga and chapter metadata. | ||||
|  */ | ||||
| fun getComicInfo( | ||||
|     manga: Manga, | ||||
|     chapter: Chapter, | ||||
|     urls: List<String>, | ||||
|     categories: List<String>?, | ||||
|     sourceName: String, | ||||
| ) = ComicInfo( | ||||
|     title = ComicInfo.Title(chapter.name), | ||||
|     series = ComicInfo.Series(manga.title), | ||||
|     number = chapter.chapterNumber.takeIf { it >= 0 }?.let { | ||||
|         if ((it.rem(1) == 0.0)) { | ||||
|             ComicInfo.Number(it.toInt().toString()) | ||||
|         } else { | ||||
|             ComicInfo.Number(it.toString()) | ||||
|         } | ||||
|     }, | ||||
|     web = ComicInfo.Web(urls.joinToString(" ")), | ||||
|     summary = manga.description?.let { ComicInfo.Summary(it) }, | ||||
|     writer = manga.author?.let { ComicInfo.Writer(it) }, | ||||
|     penciller = manga.artist?.let { ComicInfo.Penciller(it) }, | ||||
|     translator = chapter.scanlator?.let { ComicInfo.Translator(it) }, | ||||
|     genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) }, | ||||
|     publishingStatus = ComicInfo.PublishingStatusTachiyomi( | ||||
|         ComicInfoPublishingStatus.toComicInfoValue(manga.status), | ||||
|     ), | ||||
|     categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) }, | ||||
|     source = ComicInfo.SourceMihon(sourceName), | ||||
|     inker = null, | ||||
|     colorist = null, | ||||
|     letterer = null, | ||||
|     coverArtist = null, | ||||
|     tags = null, | ||||
| ) | ||||
| @@ -1,42 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import tachiyomi.domain.source.model.Pin | ||||
| import tachiyomi.domain.source.model.Pins | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
|  | ||||
| class GetEnabledSources( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<List<Source>> { | ||||
|         return combine( | ||||
|             preferences.pinnedSources().changes(), | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             preferences.disabledSources().changes(), | ||||
|             preferences.lastUsedSource().changes(), | ||||
|             repository.getSources(), | ||||
|         ) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources -> | ||||
|             sources | ||||
|                 .filter { it.lang in enabledLanguages || it.isLocal() } | ||||
|                 .filterNot { it.id.toString() in disabledSources } | ||||
|                 .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) | ||||
|                 .flatMap { | ||||
|                     val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned | ||||
|                     val source = it.copy(pin = flag) | ||||
|                     val toFlatten = mutableListOf(source) | ||||
|                     if (source.id == lastUsedSource) { | ||||
|                         toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual)) | ||||
|                     } | ||||
|                     toFlatten | ||||
|                 } | ||||
|         } | ||||
|             .distinctUntilChanged() | ||||
|     } | ||||
| } | ||||
| @@ -1,34 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import java.util.SortedMap | ||||
|  | ||||
| class GetLanguagesWithSources( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<SortedMap<String, List<Source>>> { | ||||
|         return combine( | ||||
|             preferences.enabledLanguages().changes(), | ||||
|             preferences.disabledSources().changes(), | ||||
|             repository.getOnlineSources(), | ||||
|         ) { enabledLanguage, disabledSource, onlineSources -> | ||||
|             val sortedSources = onlineSources.sortedWith( | ||||
|                 compareBy<Source> { it.id.toString() in disabledSource } | ||||
|                     .thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }, | ||||
|             ) | ||||
|  | ||||
|             sortedSources | ||||
|                 .groupBy { it.lang } | ||||
|                 .toSortedMap( | ||||
|                     compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator), | ||||
|                 ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,57 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.combine | ||||
| import tachiyomi.core.common.util.lang.compareToWithCollator | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import tachiyomi.domain.source.repository.SourceRepository | ||||
| import tachiyomi.source.local.isLocal | ||||
| import java.util.Collections | ||||
|  | ||||
| class GetSourcesWithFavoriteCount( | ||||
|     private val repository: SourceRepository, | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun subscribe(): Flow<List<Pair<Source, Long>>> { | ||||
|         return combine( | ||||
|             preferences.migrationSortingDirection().changes(), | ||||
|             preferences.migrationSortingMode().changes(), | ||||
|             repository.getSourcesWithFavoriteCount(), | ||||
|         ) { direction, mode, list -> | ||||
|             list | ||||
|                 .filterNot { it.first.isLocal() } | ||||
|                 .sortedWith(sortFn(direction, mode)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun sortFn( | ||||
|         direction: SetMigrateSorting.Direction, | ||||
|         sorting: SetMigrateSorting.Mode, | ||||
|     ): java.util.Comparator<Pair<Source, Long>> { | ||||
|         val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b -> | ||||
|             when (sorting) { | ||||
|                 SetMigrateSorting.Mode.ALPHABETICAL -> { | ||||
|                     when { | ||||
|                         a.first.isStub && !b.first.isStub -> -1 | ||||
|                         b.first.isStub && !a.first.isStub -> 1 | ||||
|                         else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase()) | ||||
|                     } | ||||
|                 } | ||||
|                 SetMigrateSorting.Mode.TOTAL -> { | ||||
|                     when { | ||||
|                         a.first.isStub && !b.first.isStub -> -1 | ||||
|                         b.first.isStub && !a.first.isStub -> 1 | ||||
|                         else -> a.second.compareTo(b.second) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return when (direction) { | ||||
|             SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn) | ||||
|             SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
|  | ||||
| class SetMigrateSorting( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(mode: Mode, direction: Direction) { | ||||
|         preferences.migrationSortingMode().set(mode) | ||||
|         preferences.migrationSortingDirection().set(direction) | ||||
|     } | ||||
|  | ||||
|     enum class Mode { | ||||
|         ALPHABETICAL, | ||||
|         TOTAL, | ||||
|     } | ||||
|  | ||||
|     enum class Direction { | ||||
|         ASCENDING, | ||||
|         DESCENDING, | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
|  | ||||
| class ToggleLanguage( | ||||
|     val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(language: String) { | ||||
|         val isEnabled = language in preferences.enabledLanguages().get() | ||||
|         preferences.enabledLanguages().getAndSet { enabled -> | ||||
|             if (isEnabled) enabled.minus(language) else enabled.plus(language) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
| import tachiyomi.domain.source.model.Source | ||||
|  | ||||
| class ToggleSource( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(source: Source, enable: Boolean = isEnabled(source.id)) { | ||||
|         await(source.id, enable) | ||||
|     } | ||||
|  | ||||
|     fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) { | ||||
|         preferences.disabledSources().getAndSet { disabled -> | ||||
|             if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun await(sourceIds: List<Long>, enable: Boolean) { | ||||
|         val transformedSourceIds = sourceIds.map { it.toString() } | ||||
|         preferences.disabledSources().getAndSet { disabled -> | ||||
|             if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun isEnabled(sourceId: Long): Boolean { | ||||
|         return sourceId.toString() in preferences.disabledSources().get() | ||||
|     } | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| package eu.kanade.domain.source.interactor | ||||
|  | ||||
| import eu.kanade.domain.source.service.SourcePreferences | ||||
| import tachiyomi.core.common.preference.getAndSet | ||||
| import tachiyomi.domain.source.model.Source | ||||
|  | ||||
| class ToggleSourcePin( | ||||
|     private val preferences: SourcePreferences, | ||||
| ) { | ||||
|  | ||||
|     fun await(source: Source) { | ||||
|         val isPinned = source.id.toString() in preferences.pinnedSources().get() | ||||
|         preferences.pinnedSources().getAndSet { pinned -> | ||||
|             if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| package eu.kanade.domain.source.model | ||||
|  | ||||
| import androidx.compose.ui.graphics.ImageBitmap | ||||
| import androidx.compose.ui.graphics.asImageBitmap | ||||
| import androidx.core.graphics.drawable.toBitmap | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import tachiyomi.domain.source.model.Source | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| val Source.icon: ImageBitmap? | ||||
|     get() { | ||||
|         return Injekt.get<ExtensionManager>().getAppIconForSource(id) | ||||
|             ?.toBitmap() | ||||
|             ?.asImageBitmap() | ||||
|     } | ||||
| @@ -1,56 +0,0 @@ | ||||
| package eu.kanade.domain.source.service | ||||
|  | ||||
| import eu.kanade.domain.source.interactor.SetMigrateSorting | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
|  | ||||
| class SourcePreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun sourceDisplayMode() = preferenceStore.getObject( | ||||
|         "pref_display_mode_catalogue", | ||||
|         LibraryDisplayMode.default, | ||||
|         LibraryDisplayMode.Serializer::serialize, | ||||
|         LibraryDisplayMode.Serializer::deserialize, | ||||
|     ) | ||||
|  | ||||
|     fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages()) | ||||
|  | ||||
|     fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet()) | ||||
|  | ||||
|     fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet()) | ||||
|  | ||||
|     fun lastUsedSource() = preferenceStore.getLong( | ||||
|         Preference.appStateKey("last_catalogue_source"), | ||||
|         -1, | ||||
|     ) | ||||
|  | ||||
|     fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true) | ||||
|  | ||||
|     fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL) | ||||
|  | ||||
|     fun migrationSortingDirection() = preferenceStore.getEnum( | ||||
|         "pref_migration_direction", | ||||
|         SetMigrateSorting.Direction.ASCENDING, | ||||
|     ) | ||||
|  | ||||
|     fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) | ||||
|  | ||||
|     fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) | ||||
|  | ||||
|     fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) | ||||
|  | ||||
|     fun trustedExtensions() = preferenceStore.getStringSet( | ||||
|         Preference.appStateKey("trusted_extensions"), | ||||
|         emptySet(), | ||||
|     ) | ||||
|  | ||||
|     fun globalSearchFilterState() = preferenceStore.getBoolean( | ||||
|         Preference.appStateKey("has_filters_toggle_state"), | ||||
|         false, | ||||
|     ) | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTracker | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.history.interactor.GetHistory | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.time.ZoneOffset | ||||
|  | ||||
| class AddTracks( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
| ) { | ||||
|  | ||||
|     // TODO: update all trackers based on common data | ||||
|     suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext { | ||||
|         withIOContext { | ||||
|             val allChapters = getChaptersByMangaId.await(mangaId) | ||||
|             val hasReadChapters = allChapters.any { it.read } | ||||
|             tracker.bind(item, hasReadChapters) | ||||
|  | ||||
|             var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext | ||||
|  | ||||
|             insertTrack.await(track) | ||||
|  | ||||
|             // TODO: merge into [SyncChapterProgressWithTrack]? | ||||
|             // Update chapter progress if newer chapters marked read locally | ||||
|             if (hasReadChapters) { | ||||
|                 val latestLocalReadChapterNumber = allChapters | ||||
|                     .sortedBy { it.chapterNumber } | ||||
|                     .takeWhile { it.read } | ||||
|                     .lastOrNull() | ||||
|                     ?.chapterNumber ?: -1.0 | ||||
|  | ||||
|                 if (latestLocalReadChapterNumber > track.lastChapterRead) { | ||||
|                     track = track.copy( | ||||
|                         lastChapterRead = latestLocalReadChapterNumber, | ||||
|                     ) | ||||
|                     tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt()) | ||||
|                 } | ||||
|  | ||||
|                 if (track.startDate <= 0) { | ||||
|                     val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId) | ||||
|                         .sortedBy { it.readAt } | ||||
|                         .firstOrNull() | ||||
|                         ?.readAt | ||||
|  | ||||
|                     firstReadChapterDate?.let { | ||||
|                         val startDate = firstReadChapterDate.time.convertEpochMillisZone( | ||||
|                             ZoneOffset.systemDefault(), | ||||
|                             ZoneOffset.UTC, | ||||
|                         ) | ||||
|                         track = track.copy( | ||||
|                             startDate = startDate, | ||||
|                         ) | ||||
|                         tracker.setRemoteStartDate(track.toDbTrack(), startDate) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             syncChapterProgressWithTrack.await(mangaId, track, tracker) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext { | ||||
|         withIOContext { | ||||
|             getTracks.await(manga.id) | ||||
|                 .filterIsInstance<EnhancedTracker>() | ||||
|                 .filter { it.accept(source) } | ||||
|                 .forEach { service -> | ||||
|                     try { | ||||
|                         service.match(manga)?.let { track -> | ||||
|                             track.manga_id = manga.id | ||||
|                             (service as Tracker).bind(track) | ||||
|                             insertTrack.await(track.toDomainTrack()!!) | ||||
|  | ||||
|                             syncChapterProgressWithTrack.await( | ||||
|                                 manga.id, | ||||
|                                 track.toDomainTrack()!!, | ||||
|                                 service, | ||||
|                             ) | ||||
|                         } | ||||
|                     } catch (e: Exception) { | ||||
|                         logcat( | ||||
|                             LogPriority.WARN, | ||||
|                             e, | ||||
|                         ) { "Could not match manga: ${manga.title} with service $service" } | ||||
|                     } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.data.track.TrackerManager | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import kotlinx.coroutines.supervisorScope | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
|  | ||||
| class RefreshTracks( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val trackerManager: TrackerManager, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack, | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
|      * Fetches updated tracking data from all logged in trackers. | ||||
|      * | ||||
|      * @return Failed updates. | ||||
|      */ | ||||
|     suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> { | ||||
|         return supervisorScope { | ||||
|             return@supervisorScope getTracks.await(mangaId) | ||||
|                 .map { it to trackerManager.get(it.trackerId) } | ||||
|                 .filter { (_, service) -> service?.isLoggedIn == true } | ||||
|                 .map { (track, service) -> | ||||
|                     async { | ||||
|                         return@async try { | ||||
|                             val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!! | ||||
|                             insertTrack.await(updatedTrack) | ||||
|                             syncChapterProgressWithTrack.await(mangaId, updatedTrack, service) | ||||
|                             null | ||||
|                         } catch (e: Throwable) { | ||||
|                             service to e | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .awaitAll() | ||||
|                 .filterNotNull() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,51 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.tachiyomi.data.track.EnhancedTracker | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId | ||||
| import tachiyomi.domain.chapter.interactor.UpdateChapter | ||||
| import tachiyomi.domain.chapter.model.toChapterUpdate | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
| import tachiyomi.domain.track.model.Track | ||||
| import kotlin.math.max | ||||
|  | ||||
| class SyncChapterProgressWithTrack( | ||||
|     private val updateChapter: UpdateChapter, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val getChaptersByMangaId: GetChaptersByMangaId, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await( | ||||
|         mangaId: Long, | ||||
|         remoteTrack: Track, | ||||
|         tracker: Tracker, | ||||
|     ) { | ||||
|         if (tracker !is EnhancedTracker) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         val sortedChapters = getChaptersByMangaId.await(mangaId) | ||||
|             .sortedBy { it.chapterNumber } | ||||
|             .filter { it.isRecognizedNumber } | ||||
|  | ||||
|         val chapterUpdates = sortedChapters | ||||
|             .filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read } | ||||
|             .map { it.copy(read = true).toChapterUpdate() } | ||||
|  | ||||
|         // only take into account continuous reading | ||||
|         val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F | ||||
|         val lastRead = max(remoteTrack.lastChapterRead, localLastRead.toDouble()) | ||||
|         val updatedTrack = remoteTrack.copy(lastChapterRead = lastRead) | ||||
|  | ||||
|         try { | ||||
|             tracker.update(updatedTrack.toDbTrack()) | ||||
|             updateChapter.awaitAll(chapterUpdates) | ||||
|             insertTrack.await(updatedTrack) | ||||
|         } catch (e: Throwable) { | ||||
|             logcat(LogPriority.WARN, e) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,59 +0,0 @@ | ||||
| package eu.kanade.domain.track.interactor | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.domain.track.model.toDbTrack | ||||
| import eu.kanade.domain.track.model.toDomainTrack | ||||
| import eu.kanade.domain.track.service.DelayedTrackingUpdateJob | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.data.track.TrackerManager | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.awaitAll | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withNonCancellableContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import tachiyomi.domain.track.interactor.InsertTrack | ||||
|  | ||||
| class TrackChapter( | ||||
|     private val getTracks: GetTracks, | ||||
|     private val trackerManager: TrackerManager, | ||||
|     private val insertTrack: InsertTrack, | ||||
|     private val delayedTrackingStore: DelayedTrackingStore, | ||||
| ) { | ||||
|  | ||||
|     suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) { | ||||
|         withNonCancellableContext { | ||||
|             val tracks = getTracks.await(mangaId) | ||||
|             if (tracks.isEmpty()) return@withNonCancellableContext | ||||
|  | ||||
|             tracks.mapNotNull { track -> | ||||
|                 val service = trackerManager.get(track.trackerId) | ||||
|                 if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) { | ||||
|                     return@mapNotNull null | ||||
|                 } | ||||
|  | ||||
|                 async { | ||||
|                     runCatching { | ||||
|                         try { | ||||
|                             val updatedTrack = service.refresh(track.toDbTrack()) | ||||
|                                 .toDomainTrack(idRequired = true)!! | ||||
|                                 .copy(lastChapterRead = chapterNumber) | ||||
|                             service.update(updatedTrack.toDbTrack(), true) | ||||
|                             insertTrack.await(updatedTrack) | ||||
|                             delayedTrackingStore.remove(track.id) | ||||
|                         } catch (e: Exception) { | ||||
|                             delayedTrackingStore.add(track.id, chapterNumber) | ||||
|                             if (setupJobOnFailure) { | ||||
|                                 DelayedTrackingUpdateJob.setupTask(context) | ||||
|                             } | ||||
|                             throw e | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|                 .awaitAll() | ||||
|                 .mapNotNull { it.exceptionOrNull() } | ||||
|                 .forEach { logcat(LogPriority.WARN, it) } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,48 +0,0 @@ | ||||
| package eu.kanade.domain.track.model | ||||
|  | ||||
| import tachiyomi.domain.track.model.Track | ||||
| import eu.kanade.tachiyomi.data.database.models.Track as DbTrack | ||||
|  | ||||
| fun Track.copyPersonalFrom(other: Track): Track { | ||||
|     return this.copy( | ||||
|         lastChapterRead = other.lastChapterRead, | ||||
|         score = other.score, | ||||
|         status = other.status, | ||||
|         startDate = other.startDate, | ||||
|         finishDate = other.finishDate, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also { | ||||
|     it.id = id | ||||
|     it.manga_id = mangaId | ||||
|     it.remote_id = remoteId | ||||
|     it.library_id = libraryId | ||||
|     it.title = title | ||||
|     it.last_chapter_read = lastChapterRead | ||||
|     it.total_chapters = totalChapters | ||||
|     it.status = status | ||||
|     it.score = score | ||||
|     it.tracking_url = remoteUrl | ||||
|     it.started_reading_date = startDate | ||||
|     it.finished_reading_date = finishDate | ||||
| } | ||||
|  | ||||
| fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { | ||||
|     val trackId = id ?: if (!idRequired) -1 else return null | ||||
|     return Track( | ||||
|         id = trackId, | ||||
|         mangaId = manga_id, | ||||
|         trackerId = tracker_id, | ||||
|         remoteId = remote_id, | ||||
|         libraryId = library_id, | ||||
|         title = title, | ||||
|         lastChapterRead = last_chapter_read, | ||||
|         totalChapters = total_chapters, | ||||
|         status = status, | ||||
|         score = score, | ||||
|         remoteUrl = tracking_url, | ||||
|         startDate = started_reading_date, | ||||
|         finishDate = finished_reading_date, | ||||
|     ) | ||||
| } | ||||
| @@ -1,72 +0,0 @@ | ||||
| package eu.kanade.domain.track.service | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.work.BackoffPolicy | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.CoroutineWorker | ||||
| import androidx.work.ExistingWorkPolicy | ||||
| import androidx.work.NetworkType | ||||
| import androidx.work.OneTimeWorkRequestBuilder | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.domain.track.interactor.TrackChapter | ||||
| import eu.kanade.domain.track.store.DelayedTrackingStore | ||||
| import eu.kanade.tachiyomi.util.system.workManager | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.lang.withIOContext | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
| import tachiyomi.domain.track.interactor.GetTracks | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     CoroutineWorker(context, workerParams) { | ||||
|  | ||||
|     override suspend fun doWork(): Result { | ||||
|         if (runAttemptCount > 3) { | ||||
|             return Result.failure() | ||||
|         } | ||||
|  | ||||
|         val getTracks = Injekt.get<GetTracks>() | ||||
|         val trackChapter = Injekt.get<TrackChapter>() | ||||
|  | ||||
|         val delayedTrackingStore = Injekt.get<DelayedTrackingStore>() | ||||
|  | ||||
|         withIOContext { | ||||
|             delayedTrackingStore.getItems() | ||||
|                 .mapNotNull { | ||||
|                     val track = getTracks.awaitOne(it.trackId) | ||||
|                     if (track == null) { | ||||
|                         delayedTrackingStore.remove(it.trackId) | ||||
|                     } | ||||
|                     track?.copy(lastChapterRead = it.lastChapterRead.toDouble()) | ||||
|                 } | ||||
|                 .forEach { track -> | ||||
|                     logcat(LogPriority.DEBUG) { | ||||
|                         "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" | ||||
|                     } | ||||
|                     trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false) | ||||
|                 } | ||||
|         } | ||||
|  | ||||
|         return if (delayedTrackingStore.getItems().isEmpty()) Result.success() else Result.retry() | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         private const val TAG = "DelayedTrackingUpdate" | ||||
|  | ||||
|         fun setupTask(context: Context) { | ||||
|             val constraints = Constraints( | ||||
|                 requiredNetworkType = NetworkType.CONNECTED, | ||||
|             ) | ||||
|  | ||||
|             val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>() | ||||
|                 .setConstraints(constraints) | ||||
|                 .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES) | ||||
|                 .addTag(TAG) | ||||
|                 .build() | ||||
|  | ||||
|             context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,38 +0,0 @@ | ||||
| package eu.kanade.domain.track.service | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.track.Tracker | ||||
| import eu.kanade.tachiyomi.data.track.anilist.Anilist | ||||
| import tachiyomi.core.common.preference.Preference | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
|  | ||||
| class TrackPreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun trackUsername(tracker: Tracker) = preferenceStore.getString( | ||||
|         Preference.privateKey("pref_mangasync_username_${tracker.id}"), | ||||
|         "", | ||||
|     ) | ||||
|  | ||||
|     fun trackPassword(tracker: Tracker) = preferenceStore.getString( | ||||
|         Preference.privateKey("pref_mangasync_password_${tracker.id}"), | ||||
|         "", | ||||
|     ) | ||||
|  | ||||
|     fun trackAuthExpired(tracker: Tracker) = preferenceStore.getBoolean( | ||||
|         Preference.privateKey("pref_tracker_auth_expired_${tracker.id}"), | ||||
|         false, | ||||
|     ) | ||||
|  | ||||
|     fun setCredentials(tracker: Tracker, username: String, password: String) { | ||||
|         trackUsername(tracker).set(username) | ||||
|         trackPassword(tracker).set(password) | ||||
|         trackAuthExpired(tracker).set(false) | ||||
|     } | ||||
|  | ||||
|     fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "") | ||||
|  | ||||
|     fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) | ||||
|  | ||||
|     fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| package eu.kanade.domain.track.store | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.core.content.edit | ||||
| import logcat.LogPriority | ||||
| import tachiyomi.core.common.util.system.logcat | ||||
|  | ||||
| class DelayedTrackingStore(context: Context) { | ||||
|  | ||||
|     /** | ||||
|      * Preference file where queued tracking updates are stored. | ||||
|      */ | ||||
|     private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) | ||||
|  | ||||
|     fun add(trackId: Long, lastChapterRead: Double) { | ||||
|         val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f) | ||||
|         if (lastChapterRead > previousLastChapterRead) { | ||||
|             logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" } | ||||
|             preferences.edit { | ||||
|                 putFloat(trackId.toString(), lastChapterRead.toFloat()) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun remove(trackId: Long) { | ||||
|         preferences.edit { | ||||
|             remove(trackId.toString()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getItems(): List<DelayedTrackingItem> { | ||||
|         return preferences.all.mapNotNull { | ||||
|             DelayedTrackingItem( | ||||
|                 trackId = it.key.toLong(), | ||||
|                 lastChapterRead = it.value.toString().toFloat(), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     data class DelayedTrackingItem( | ||||
|         val trackId: Long, | ||||
|         val lastChapterRead: Float, | ||||
|     ) | ||||
| } | ||||
| @@ -1,43 +0,0 @@ | ||||
| package eu.kanade.domain.ui | ||||
|  | ||||
| import eu.kanade.domain.ui.model.AppTheme | ||||
| import eu.kanade.domain.ui.model.TabletUiMode | ||||
| import eu.kanade.domain.ui.model.ThemeMode | ||||
| import eu.kanade.tachiyomi.util.system.DeviceUtil | ||||
| import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable | ||||
| import tachiyomi.core.common.preference.PreferenceStore | ||||
| import tachiyomi.core.common.preference.getEnum | ||||
| import java.time.format.DateTimeFormatter | ||||
| import java.time.format.FormatStyle | ||||
| import java.util.Locale | ||||
|  | ||||
| class UiPreferences( | ||||
|     private val preferenceStore: PreferenceStore, | ||||
| ) { | ||||
|  | ||||
|     fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM) | ||||
|  | ||||
|     fun appTheme() = preferenceStore.getEnum( | ||||
|         "pref_app_theme", | ||||
|         if (DeviceUtil.isDynamicColorAvailable) { | ||||
|             AppTheme.MONET | ||||
|         } else { | ||||
|             AppTheme.DEFAULT | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false) | ||||
|  | ||||
|     fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true) | ||||
|  | ||||
|     fun dateFormat() = preferenceStore.getString("app_date_format", "") | ||||
|  | ||||
|     fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) | ||||
|  | ||||
|     companion object { | ||||
|         fun dateFormat(format: String): DateTimeFormatter = when (format) { | ||||
|             "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) | ||||
|             else -> DateTimeFormatter.ofPattern(format, Locale.getDefault()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.isPreviewBuildType | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| enum class AppTheme(val titleRes: StringResource?) { | ||||
|     DEFAULT(MR.strings.label_default), | ||||
|     MONET(MR.strings.theme_monet), | ||||
|     GREEN_APPLE(MR.strings.theme_greenapple), | ||||
|     LAVENDER(MR.strings.theme_lavender), | ||||
|     MIDNIGHT_DUSK(MR.strings.theme_midnightdusk), | ||||
|  | ||||
|     // TODO: re-enable for preview | ||||
|     NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }), | ||||
|     STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri), | ||||
|     TAKO(MR.strings.theme_tako), | ||||
|     TEALTURQUOISE(MR.strings.theme_tealturquoise), | ||||
|     TIDAL_WAVE(MR.strings.theme_tidalwave), | ||||
|     YINYANG(MR.strings.theme_yinyang), | ||||
|     YOTSUBA(MR.strings.theme_yotsuba), | ||||
|  | ||||
|     // Deprecated | ||||
|     DARK_BLUE(null), | ||||
|     HOT_PINK(null), | ||||
|     BLUE(null), | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import dev.icerock.moko.resources.StringResource | ||||
| import tachiyomi.i18n.MR | ||||
|  | ||||
| enum class TabletUiMode(val titleRes: StringResource) { | ||||
|     AUTOMATIC(MR.strings.automatic_background), | ||||
|     ALWAYS(MR.strings.lock_always), | ||||
|     LANDSCAPE(MR.strings.landscape), | ||||
|     NEVER(MR.strings.lock_never), | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| package eu.kanade.domain.ui.model | ||||
|  | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
|  | ||||
| enum class ThemeMode { | ||||
|     LIGHT, | ||||
|     DARK, | ||||
|     SYSTEM, | ||||
| } | ||||
|  | ||||
| fun setAppCompatDelegateThemeMode(themeMode: ThemeMode) { | ||||
|     AppCompatDelegate.setDefaultNightMode( | ||||
|         when (themeMode) { | ||||
|             ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO | ||||
|             ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES | ||||
|             ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -1,168 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.grid.GridCells | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.HelpOutline | ||||
| import androidx.compose.material.icons.outlined.Public | ||||
| import androidx.compose.material.icons.outlined.Refresh | ||||
| import androidx.compose.material3.SnackbarDuration | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.material3.SnackbarResult | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.paging.LoadState | ||||
| import androidx.paging.compose.LazyPagingItems | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid | ||||
| import eu.kanade.presentation.browse.components.BrowseSourceList | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.util.formattedMessage | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import tachiyomi.core.common.i18n.stringResource | ||||
| import tachiyomi.domain.library.model.LibraryDisplayMode | ||||
| import tachiyomi.domain.manga.model.Manga | ||||
| import tachiyomi.domain.source.model.StubSource | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
| import tachiyomi.presentation.core.screens.EmptyScreenAction | ||||
| import tachiyomi.presentation.core.screens.LoadingScreen | ||||
| import tachiyomi.source.local.LocalSource | ||||
|  | ||||
| @Composable | ||||
| fun BrowseSourceContent( | ||||
|     source: Source?, | ||||
|     mangaList: LazyPagingItems<StateFlow<Manga>>, | ||||
|     columns: GridCells, | ||||
|     displayMode: LibraryDisplayMode, | ||||
|     snackbarHostState: SnackbarHostState, | ||||
|     contentPadding: PaddingValues, | ||||
|     onWebViewClick: () -> Unit, | ||||
|     onHelpClick: () -> Unit, | ||||
|     onLocalSourceHelpClick: () -> Unit, | ||||
|     onMangaClick: (Manga) -> Unit, | ||||
|     onMangaLongClick: (Manga) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error } | ||||
|         ?: mangaList.loadState.append.takeIf { it is LoadState.Error } | ||||
|  | ||||
|     val getErrorMessage: (LoadState.Error) -> String = { state -> | ||||
|         with(context) { state.error.formattedMessage } | ||||
|     } | ||||
|  | ||||
|     LaunchedEffect(errorState) { | ||||
|         if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) { | ||||
|             val result = snackbarHostState.showSnackbar( | ||||
|                 message = getErrorMessage(errorState), | ||||
|                 actionLabel = context.stringResource(MR.strings.action_retry), | ||||
|                 duration = SnackbarDuration.Indefinite, | ||||
|             ) | ||||
|             when (result) { | ||||
|                 SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss() | ||||
|                 SnackbarResult.ActionPerformed -> mangaList.retry() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) { | ||||
|         EmptyScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|             message = getErrorMessage(errorState), | ||||
|             actions = if (source is LocalSource) { | ||||
|                 persistentListOf( | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.local_source_help_guide, | ||||
|                         icon = Icons.AutoMirrored.Outlined.HelpOutline, | ||||
|                         onClick = onLocalSourceHelpClick, | ||||
|                     ), | ||||
|                 ) | ||||
|             } else { | ||||
|                 persistentListOf( | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.action_retry, | ||||
|                         icon = Icons.Outlined.Refresh, | ||||
|                         onClick = mangaList::refresh, | ||||
|                     ), | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.action_open_in_web_view, | ||||
|                         icon = Icons.Outlined.Public, | ||||
|                         onClick = onWebViewClick, | ||||
|                     ), | ||||
|                     EmptyScreenAction( | ||||
|                         stringRes = MR.strings.label_help, | ||||
|                         icon = Icons.AutoMirrored.Outlined.HelpOutline, | ||||
|                         onClick = onHelpClick, | ||||
|                     ), | ||||
|                 ) | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) { | ||||
|         LoadingScreen( | ||||
|             modifier = Modifier.padding(contentPadding), | ||||
|         ) | ||||
|         return | ||||
|     } | ||||
|  | ||||
|     when (displayMode) { | ||||
|         LibraryDisplayMode.ComfortableGrid -> { | ||||
|             BrowseSourceComfortableGrid( | ||||
|                 mangaList = mangaList, | ||||
|                 columns = columns, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|         LibraryDisplayMode.List -> { | ||||
|             BrowseSourceList( | ||||
|                 mangaList = mangaList, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|         LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> { | ||||
|             BrowseSourceCompactGrid( | ||||
|                 mangaList = mangaList, | ||||
|                 columns = columns, | ||||
|                 contentPadding = contentPadding, | ||||
|                 onMangaClick = onMangaClick, | ||||
|                 onMangaLongClick = onMangaLongClick, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| internal fun MissingSourceScreen( | ||||
|     source: StubSource, | ||||
|     navigateUp: () -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = source.name, | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         EmptyScreen( | ||||
|             message = stringResource(MR.strings.source_not_installed, source.toString()), | ||||
|             modifier = Modifier.padding(paddingValues), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -1,445 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import android.provider.Settings | ||||
| import android.util.DisplayMetrics | ||||
| import androidx.compose.foundation.clickable | ||||
| import androidx.compose.foundation.layout.Arrangement | ||||
| import androidx.compose.foundation.layout.Column | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.Row | ||||
| import androidx.compose.foundation.layout.fillMaxWidth | ||||
| import androidx.compose.foundation.layout.height | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.layout.size | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.automirrored.outlined.Launch | ||||
| import androidx.compose.material.icons.outlined.Settings | ||||
| import androidx.compose.material3.AlertDialog | ||||
| import androidx.compose.material3.Button | ||||
| import androidx.compose.material3.HorizontalDivider | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.MaterialTheme | ||||
| import androidx.compose.material3.OutlinedButton | ||||
| import androidx.compose.material3.Switch | ||||
| import androidx.compose.material3.Text | ||||
| import androidx.compose.material3.TextButton | ||||
| import androidx.compose.material3.VerticalDivider | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.platform.LocalUriHandler | ||||
| import androidx.compose.ui.text.TextStyle | ||||
| import androidx.compose.ui.text.font.FontWeight | ||||
| import androidx.compose.ui.text.style.TextAlign | ||||
| import androidx.compose.ui.unit.dp | ||||
| import eu.kanade.domain.extension.interactor.ExtensionSourceItem | ||||
| import eu.kanade.presentation.browse.components.ExtensionIcon | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.components.AppBarActions | ||||
| import eu.kanade.presentation.components.WarningBanner | ||||
| import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget | ||||
| import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer | ||||
| import eu.kanade.tachiyomi.extension.model.Extension | ||||
| import eu.kanade.tachiyomi.source.ConfigurableSource | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import eu.kanade.tachiyomi.util.system.copyToClipboard | ||||
| import kotlinx.collections.immutable.ImmutableList | ||||
| import kotlinx.collections.immutable.persistentListOf | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.ScrollbarLazyColumn | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.components.material.padding | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionDetailsScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     state: ExtensionDetailsScreenModel.State, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickEnableAll: () -> Unit, | ||||
|     onClickDisableAll: () -> Unit, | ||||
|     onClickClearCookies: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
| ) { | ||||
|     val uriHandler = LocalUriHandler.current | ||||
|     val url = remember(state.extension) { | ||||
|         val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() | ||||
|         regex.find(state.extension?.repoUrl.orEmpty()) | ||||
|             ?.let { | ||||
|                 val (user, repo) = it.destructured | ||||
|                 "https://github.com/$user/$repo" | ||||
|             } | ||||
|             ?: state.extension?.repoUrl | ||||
|     } | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.label_extension_info), | ||||
|                 navigateUp = navigateUp, | ||||
|                 actions = { | ||||
|                     AppBarActions( | ||||
|                         actions = persistentListOf<AppBar.AppBarAction>().builder() | ||||
|                             .apply { | ||||
|                                 if (url != null) { | ||||
|                                     add( | ||||
|                                         AppBar.Action( | ||||
|                                             title = stringResource(MR.strings.action_open_repo), | ||||
|                                             icon = Icons.AutoMirrored.Outlined.Launch, | ||||
|                                             onClick = { | ||||
|                                                 uriHandler.openUri(url) | ||||
|                                             }, | ||||
|                                         ), | ||||
|                                     ) | ||||
|                                 } | ||||
|                                 addAll( | ||||
|                                     listOf( | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_enable_all), | ||||
|                                             onClick = onClickEnableAll, | ||||
|                                         ), | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.action_disable_all), | ||||
|                                             onClick = onClickDisableAll, | ||||
|                                         ), | ||||
|                                         AppBar.OverflowAction( | ||||
|                                             title = stringResource(MR.strings.pref_clear_cookies), | ||||
|                                             onClick = onClickClearCookies, | ||||
|                                         ), | ||||
|                                     ), | ||||
|                                 ) | ||||
|                             } | ||||
|                             .build(), | ||||
|                     ) | ||||
|                 }, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { paddingValues -> | ||||
|         if (state.extension == null) { | ||||
|             EmptyScreen( | ||||
|                 MR.strings.empty_screen, | ||||
|                 modifier = Modifier.padding(paddingValues), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|  | ||||
|         ExtensionDetails( | ||||
|             contentPadding = paddingValues, | ||||
|             extension = state.extension, | ||||
|             sources = state.sources, | ||||
|             onClickSourcePreferences = onClickSourcePreferences, | ||||
|             onClickUninstall = onClickUninstall, | ||||
|             onClickSource = onClickSource, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionDetails( | ||||
|     contentPadding: PaddingValues, | ||||
|     extension: Extension.Installed, | ||||
|     sources: ImmutableList<ExtensionSourceItem>, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     var showNsfwWarning by remember { mutableStateOf(false) } | ||||
|  | ||||
|     ScrollbarLazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         if (extension.isObsolete) { | ||||
|             item { | ||||
|                 WarningBanner(MR.strings.obsolete_extension_message) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         item { | ||||
|             DetailsHeader( | ||||
|                 extension = extension, | ||||
|                 onClickUninstall = onClickUninstall, | ||||
|                 onClickAppInfo = { | ||||
|                     Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { | ||||
|                         data = Uri.fromParts("package", extension.pkgName, null) | ||||
|                         context.startActivity(this) | ||||
|                     } | ||||
|                     Unit | ||||
|                 }.takeIf { extension.isShared }, | ||||
|                 onClickAgeRating = { | ||||
|                     showNsfwWarning = true | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         items( | ||||
|             items = sources, | ||||
|             key = { it.source.id }, | ||||
|         ) { source -> | ||||
|             SourceSwitchPreference( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 source = source, | ||||
|                 onClickSourcePreferences = onClickSourcePreferences, | ||||
|                 onClickSource = onClickSource, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|     if (showNsfwWarning) { | ||||
|         NsfwWarningDialog( | ||||
|             onClickConfirm = { | ||||
|                 showNsfwWarning = false | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun DetailsHeader( | ||||
|     extension: Extension, | ||||
|     onClickAgeRating: () -> Unit, | ||||
|     onClickUninstall: () -> Unit, | ||||
|     onClickAppInfo: (() -> Unit)?, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     Column { | ||||
|         Column( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding( | ||||
|                     start = MaterialTheme.padding.medium, | ||||
|                     end = MaterialTheme.padding.medium, | ||||
|                     top = MaterialTheme.padding.medium, | ||||
|                     bottom = MaterialTheme.padding.small, | ||||
|                 ) | ||||
|                 .clickable { | ||||
|                     val extDebugInfo = buildString { | ||||
|                         append( | ||||
|                             """ | ||||
|                             Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName}) | ||||
|                             Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode}) | ||||
|                             NSFW: ${extension.isNsfw} | ||||
|                             """.trimIndent(), | ||||
|                         ) | ||||
|  | ||||
|                         if (extension is Extension.Installed) { | ||||
|                             append("\n\n") | ||||
|                             append( | ||||
|                                 """ | ||||
|                                 Update available: ${extension.hasUpdate} | ||||
|                                 Obsolete: ${extension.isObsolete} | ||||
|                                 Shared: ${extension.isShared} | ||||
|                                 Repository: ${extension.repoUrl} | ||||
|                                 """.trimIndent(), | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     context.copyToClipboard("Extension Debug information", extDebugInfo) | ||||
|                 }, | ||||
|             horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         ) { | ||||
|             ExtensionIcon( | ||||
|                 modifier = Modifier | ||||
|                     .size(112.dp), | ||||
|                 extension = extension, | ||||
|                 density = DisplayMetrics.DENSITY_XXXHIGH, | ||||
|             ) | ||||
|  | ||||
|             Text( | ||||
|                 text = extension.name, | ||||
|                 style = MaterialTheme.typography.headlineSmall, | ||||
|                 textAlign = TextAlign.Center, | ||||
|             ) | ||||
|  | ||||
|             val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.") | ||||
|  | ||||
|             Text( | ||||
|                 text = strippedPkgName, | ||||
|                 style = MaterialTheme.typography.bodySmall, | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier | ||||
|                 .fillMaxWidth() | ||||
|                 .padding( | ||||
|                     horizontal = MaterialTheme.padding.extraLarge, | ||||
|                     vertical = MaterialTheme.padding.small, | ||||
|                 ), | ||||
|             horizontalArrangement = Arrangement.SpaceEvenly, | ||||
|             verticalAlignment = Alignment.CenterVertically, | ||||
|         ) { | ||||
|             InfoText( | ||||
|                 modifier = Modifier.weight(1f), | ||||
|                 primaryText = extension.versionName, | ||||
|                 secondaryText = stringResource(MR.strings.ext_info_version), | ||||
|             ) | ||||
|  | ||||
|             InfoDivider() | ||||
|  | ||||
|             InfoText( | ||||
|                 modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f), | ||||
|                 primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context), | ||||
|                 secondaryText = stringResource(MR.strings.ext_info_language), | ||||
|             ) | ||||
|  | ||||
|             if (extension.isNsfw) { | ||||
|                 InfoDivider() | ||||
|  | ||||
|                 InfoText( | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     primaryText = stringResource(MR.strings.ext_nsfw_short), | ||||
|                     primaryTextStyle = MaterialTheme.typography.bodyLarge.copy( | ||||
|                         color = MaterialTheme.colorScheme.error, | ||||
|                         fontWeight = FontWeight.Medium, | ||||
|                     ), | ||||
|                     secondaryText = stringResource(MR.strings.ext_info_age_rating), | ||||
|                     onClick = onClickAgeRating, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Row( | ||||
|             modifier = Modifier.padding( | ||||
|                 start = MaterialTheme.padding.medium, | ||||
|                 end = MaterialTheme.padding.medium, | ||||
|                 top = MaterialTheme.padding.small, | ||||
|                 bottom = MaterialTheme.padding.medium, | ||||
|             ), | ||||
|             horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium), | ||||
|         ) { | ||||
|             OutlinedButton( | ||||
|                 modifier = Modifier.weight(1f), | ||||
|                 onClick = onClickUninstall, | ||||
|             ) { | ||||
|                 Text(stringResource(MR.strings.ext_uninstall)) | ||||
|             } | ||||
|  | ||||
|             if (onClickAppInfo != null) { | ||||
|                 Button( | ||||
|                     modifier = Modifier.weight(1f), | ||||
|                     onClick = onClickAppInfo, | ||||
|                 ) { | ||||
|                     Text( | ||||
|                         text = stringResource(MR.strings.ext_app_info), | ||||
|                         color = MaterialTheme.colorScheme.onPrimary, | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         HorizontalDivider() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun InfoText( | ||||
|     primaryText: String, | ||||
|     secondaryText: String, | ||||
|     modifier: Modifier = Modifier, | ||||
|     primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge, | ||||
|     onClick: (() -> Unit)? = null, | ||||
| ) { | ||||
|     val clickableModifier = if (onClick != null) { | ||||
|         Modifier.clickable(interactionSource = null, indication = null, onClick = onClick) | ||||
|     } else { | ||||
|         Modifier | ||||
|     } | ||||
|  | ||||
|     Column( | ||||
|         modifier = modifier.then(clickableModifier), | ||||
|         horizontalAlignment = Alignment.CenterHorizontally, | ||||
|         verticalArrangement = Arrangement.Center, | ||||
|     ) { | ||||
|         Text( | ||||
|             text = primaryText, | ||||
|             textAlign = TextAlign.Center, | ||||
|             style = primaryTextStyle, | ||||
|         ) | ||||
|  | ||||
|         Text( | ||||
|             text = secondaryText + if (onClick != null) " ⓘ" else "", | ||||
|             textAlign = TextAlign.Center, | ||||
|             style = MaterialTheme.typography.bodyMedium, | ||||
|             color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun InfoDivider() { | ||||
|     VerticalDivider( | ||||
|         modifier = Modifier.height(20.dp), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun SourceSwitchPreference( | ||||
|     source: ExtensionSourceItem, | ||||
|     onClickSourcePreferences: (sourceId: Long) -> Unit, | ||||
|     onClickSource: (sourceId: Long) -> Unit, | ||||
|     modifier: Modifier = Modifier, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     TextPreferenceWidget( | ||||
|         modifier = modifier, | ||||
|         title = if (source.labelAsName) { | ||||
|             source.source.toString() | ||||
|         } else { | ||||
|             LocaleHelper.getSourceDisplayName(source.source.lang, context) | ||||
|         }, | ||||
|         widget = { | ||||
|             Row( | ||||
|                 verticalAlignment = Alignment.CenterVertically, | ||||
|             ) { | ||||
|                 if (source.source is ConfigurableSource) { | ||||
|                     IconButton(onClick = { onClickSourcePreferences(source.source.id) }) { | ||||
|                         Icon( | ||||
|                             imageVector = Icons.Outlined.Settings, | ||||
|                             contentDescription = stringResource(MR.strings.label_settings), | ||||
|                             tint = MaterialTheme.colorScheme.onSurface, | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Switch( | ||||
|                     checked = source.enabled, | ||||
|                     onCheckedChange = null, | ||||
|                     modifier = Modifier.padding(start = TrailingWidgetBuffer), | ||||
|                 ) | ||||
|             } | ||||
|         }, | ||||
|         onPreferenceClick = { onClickSource(source.source.id) }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun NsfwWarningDialog( | ||||
|     onClickConfirm: () -> Unit, | ||||
| ) { | ||||
|     AlertDialog( | ||||
|         text = { | ||||
|             Text(text = stringResource(MR.strings.ext_nsfw_warning)) | ||||
|         }, | ||||
|         confirmButton = { | ||||
|             TextButton(onClick = onClickConfirm) { | ||||
|                 Text(text = stringResource(MR.strings.action_ok)) | ||||
|             } | ||||
|         }, | ||||
|         onDismissRequest = onClickConfirm, | ||||
|     ) | ||||
| } | ||||
| @@ -1,68 +0,0 @@ | ||||
| package eu.kanade.presentation.browse | ||||
|  | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.foundation.lazy.LazyColumn | ||||
| import androidx.compose.foundation.lazy.items | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import eu.kanade.presentation.components.AppBar | ||||
| import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget | ||||
| import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import tachiyomi.i18n.MR | ||||
| import tachiyomi.presentation.core.components.material.Scaffold | ||||
| import tachiyomi.presentation.core.i18n.stringResource | ||||
| import tachiyomi.presentation.core.screens.EmptyScreen | ||||
|  | ||||
| @Composable | ||||
| fun ExtensionFilterScreen( | ||||
|     navigateUp: () -> Unit, | ||||
|     state: ExtensionFilterState.Success, | ||||
|     onClickToggle: (String) -> Unit, | ||||
| ) { | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             AppBar( | ||||
|                 title = stringResource(MR.strings.label_extensions), | ||||
|                 navigateUp = navigateUp, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|     ) { contentPadding -> | ||||
|         if (state.isEmpty) { | ||||
|             EmptyScreen( | ||||
|                 stringRes = MR.strings.empty_screen, | ||||
|                 modifier = Modifier.padding(contentPadding), | ||||
|             ) | ||||
|             return@Scaffold | ||||
|         } | ||||
|         ExtensionFilterContent( | ||||
|             contentPadding = contentPadding, | ||||
|             state = state, | ||||
|             onClickLang = onClickToggle, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @Composable | ||||
| private fun ExtensionFilterContent( | ||||
|     contentPadding: PaddingValues, | ||||
|     state: ExtensionFilterState.Success, | ||||
|     onClickLang: (String) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|     LazyColumn( | ||||
|         contentPadding = contentPadding, | ||||
|     ) { | ||||
|         items(state.languages) { language -> | ||||
|             SwitchPreferenceWidget( | ||||
|                 modifier = Modifier.animateItem(), | ||||
|                 title = LocaleHelper.getSourceDisplayName(language, context), | ||||
|                 checked = language in state.enabledLanguages, | ||||
|                 onCheckedChanged = { onClickLang(language) }, | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||