Compare commits
	
		
			542 Commits
		
	
	
		
			v1.5.1.1
			...
			v171901002
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f6317f566e | ||
| 
						 | 
					9f51e4e6a5 | ||
| 
						 | 
					750604a31f | ||
| 
						 | 
					392eee0ad4 | ||
| 
						 | 
					37e7b987ee | ||
| 
						 | 
					9eac51e729 | ||
| 
						 | 
					fa9cce6783 | ||
| 
						 | 
					f0d4b63a97 | ||
| 
						 | 
					83eeb11388 | ||
| 
						 | 
					01f746f33d | ||
| 
						 | 
					200851894b | ||
| 
						 | 
					862e5cf4ab | ||
| 
						 | 
					0b07f2a407 | ||
| 
						 | 
					9ba6feef0b | ||
| 
						 | 
					63a0638522 | ||
| 
						 | 
					f9a4e6e363 | ||
| 
						 | 
					6b40fd4bdc | ||
| 
						 | 
					04c7776466 | ||
| 
						 | 
					92c335b4e1 | ||
| 
						 | 
					17251e576b | ||
| 
						 | 
					62ea782429 | ||
| 
						 | 
					f99474e3c1 | ||
| 
						 | 
					57ac8f428f | ||
| 
						 | 
					9cc1adbf15 | ||
| 
						 | 
					1d9a440ae7 | ||
| 
						 | 
					511553806c | ||
| 
						 | 
					87e7d7c4fe | ||
| 
						 | 
					ec87089310 | ||
| 
						 | 
					d8478ebb01 | ||
| 
						 | 
					600adc81b5 | ||
| 
						 | 
					ddac2870af | ||
| 8d9c8c1394 | |||
| 
						 | 
					b59c3bcb23 | ||
| 7f554adba5 | |||
| 
						 | 
					21ce061282 | ||
| 
						 | 
					bdb71e9b14 | ||
| 
						 | 
					df22e7de15 | ||
| 
						 | 
					6b3550396b | ||
| 
						 | 
					c70f1e31a6 | ||
| 
						 | 
					695670e944 | ||
| 
						 | 
					1028826788 | ||
| 
						 | 
					82a8977c96 | ||
| 
						 | 
					07d9ce1054 | ||
| 
						 | 
					7da7d49277 | ||
| 
						 | 
					9b45365441 | ||
| 
						 | 
					91a7464bce | ||
| 
						 | 
					51add226eb | ||
| 
						 | 
					332e9f5108 | ||
| 
						 | 
					0b91087c07 | ||
| 
						 | 
					ebbb1ba0f8 | ||
| 
						 | 
					e9143ae852 | ||
| 
						 | 
					42e8ecee78 | ||
| 
						 | 
					4efd76fcbc | ||
| 
						 | 
					fb1614070e | ||
| 
						 | 
					c473dd7227 | ||
| 
						 | 
					76bddb195d | ||
| 
						 | 
					1e02ad2041 | ||
| 
						 | 
					f6ab909f8b | ||
| 
						 | 
					7e520e9bed | ||
| 
						 | 
					32e2d05014 | ||
| 
						 | 
					40d9c97f73 | ||
| 
						 | 
					1aa68d3449 | ||
| 
						 | 
					aeeac8cccd | ||
| 
						 | 
					7292edf997 | ||
| 
						 | 
					f49256c72f | ||
| 
						 | 
					d02b28b81f | ||
| 
						 | 
					08117043dd | ||
| 
						 | 
					63496c993e | ||
| 
						 | 
					00ef542e49 | ||
| 
						 | 
					a78c6e6b33 | ||
| 
						 | 
					363eaf9bf9 | ||
| 
						 | 
					fec6683701 | ||
| 
						 | 
					1549edb647 | ||
| 
						 | 
					3de48ba162 | ||
| 
						 | 
					a2a3d6f1a7 | ||
| 
						 | 
					ccab2c7648 | ||
| 
						 | 
					880dd1db5c | ||
| 
						 | 
					ed18fea356 | ||
| 
						 | 
					9816b20bf6 | ||
| 
						 | 
					0bb2195bff | ||
| 
						 | 
					ab2d0c4036 | ||
| 
						 | 
					99fc417109 | ||
| 
						 | 
					dc304ef8c1 | ||
| 
						 | 
					c5511880bc | ||
| 
						 | 
					5fe76d735e | ||
| 
						 | 
					3064b3b835 | ||
| 
						 | 
					70dc8af3ce | ||
| 
						 | 
					53c8c241da | ||
| 
						 | 
					bdc4f5680b | ||
| 
						 | 
					ed290573b2 | ||
| 
						 | 
					1616a97a8a | ||
| 
						 | 
					d090183007 | ||
| 
						 | 
					de337fd260 | ||
| 
						 | 
					12dc206323 | ||
| 
						 | 
					d47c508dee | ||
| 
						 | 
					ed75f55437 | ||
| 
						 | 
					5ad3ad4a57 | ||
| 
						 | 
					aeac1bd1d4 | ||
| 
						 | 
					4d18085072 | ||
| 
						 | 
					0c9f8214ca | ||
| 
						 | 
					a7ce7ce02e | ||
| 
						 | 
					820986c7f0 | ||
| 
						 | 
					8079cae745 | ||
| 
						 | 
					6f067bd258 | ||
| 
						 | 
					b6ade0f212 | ||
| 
						 | 
					27dadc1be3 | ||
| 
						 | 
					95e4162b4c | ||
| 
						 | 
					f75557585e | ||
| 
						 | 
					1b4c26919b | ||
| 
						 | 
					ad085bf129 | ||
| 
						 | 
					8fcd551105 | ||
| 
						 | 
					a0954700e2 | ||
| 
						 | 
					9705560442 | ||
| 
						 | 
					1f47a13ce5 | ||
| 
						 | 
					6f0ff2c975 | ||
| 
						 | 
					76e5477986 | ||
| 
						 | 
					7f308d5be3 | ||
| 
						 | 
					54a43c83e8 | ||
| 
						 | 
					8fe7266c84 | ||
| 
						 | 
					d7a46b27b7 | ||
| 
						 | 
					2257d09fdd | ||
| 
						 | 
					047c5481c4 | ||
| 
						 | 
					8a6719f934 | ||
| 
						 | 
					51a692f3be | ||
| 
						 | 
					b333f93171 | ||
| 
						 | 
					89d34a1a71 | ||
| 
						 | 
					8788e920ce | ||
| 
						 | 
					d306fb53d3 | ||
| 
						 | 
					374537b5c7 | ||
| 
						 | 
					598149d4cd | ||
| 
						 | 
					50bcf18096 | ||
| 
						 | 
					a089ced03f | ||
| 
						 | 
					1f18dddf8b | ||
| 
						 | 
					f5934e240e | ||
| 
						 | 
					6b8da2eacf | ||
| 
						 | 
					f4757a67b7 | ||
| 
						 | 
					6edeb9d840 | ||
| 
						 | 
					43ce0fd7bc | ||
| 
						 | 
					5599f5a8fc | ||
| 
						 | 
					6fd45ceb4f | ||
| 
						 | 
					05ad8aac29 | ||
| 
						 | 
					fa4f2476b7 | ||
| 
						 | 
					00818a94e9 | ||
| 
						 | 
					5d5250e44a | ||
| 
						 | 
					3052b33132 | ||
| 
						 | 
					50de6f8b5b | ||
| 
						 | 
					f88a2f415f | ||
| 
						 | 
					96f9813e01 | ||
| 
						 | 
					fee739cb17 | ||
| 
						 | 
					b1814c63b9 | ||
| 
						 | 
					c1d45678f8 | ||
| 
						 | 
					3d34e59a94 | ||
| 
						 | 
					f1133bea8b | ||
| 
						 | 
					ec64c88ff1 | ||
| 
						 | 
					be66dbba6c | ||
| 
						 | 
					8926cdbbf5 | ||
| 
						 | 
					a956870dec | ||
| 
						 | 
					8ed7951c9b | ||
| 
						 | 
					5569a47674 | ||
| 
						 | 
					0dc6981913 | ||
| 
						 | 
					4984f2f7ad | ||
| 
						 | 
					3b6891c84a | ||
| 
						 | 
					4901e7174c | ||
| 
						 | 
					8d70e68fe2 | ||
| 
						 | 
					d3e1527b70 | ||
| 
						 | 
					0c201301f2 | ||
| 
						 | 
					6090590f24 | ||
| 
						 | 
					06b88c783d | ||
| 
						 | 
					bb75ebf635 | ||
| 
						 | 
					7d7d0014be | ||
| 
						 | 
					b3f8d44794 | ||
| 
						 | 
					29d1e38340 | ||
| 
						 | 
					2be872e61e | ||
| 
						 | 
					377c5518f7 | ||
| 
						 | 
					21be7357b5 | ||
| 
						 | 
					d47ba2c820 | ||
| 
						 | 
					a64b14614a | ||
| 
						 | 
					6a88192e77 | ||
| 
						 | 
					aa7c630818 | ||
| 
						 | 
					7fb54f14c7 | ||
| 
						 | 
					3d709c02b7 | ||
| 
						 | 
					339d384561 | ||
| 
						 | 
					50338d51af | ||
| 
						 | 
					92dbabf899 | ||
| 
						 | 
					0043021390 | ||
| 
						 | 
					70ba9b20da | ||
| 
						 | 
					7fda0a04a1 | ||
| 
						 | 
					3db3157dc9 | ||
| 
						 | 
					2089fe60ca | ||
| 
						 | 
					9606d36670 | ||
| 
						 | 
					869cf64c54 | ||
| 
						 | 
					f57ec1f6c0 | ||
| 
						 | 
					361eea9a06 | ||
| 
						 | 
					838b4056ac | ||
| 
						 | 
					0c0a98510b | ||
| 
						 | 
					be642ed06f | ||
| 
						 | 
					fd77f38e95 | ||
| 
						 | 
					c9baab7267 | ||
| 
						 | 
					86985cfd5b | ||
| 
						 | 
					1327a4e069 | ||
| 
						 | 
					c46acbc579 | ||
| 
						 | 
					4c6a403fae | ||
| 
						 | 
					78920022bd | ||
| 
						 | 
					7b16c41e82 | ||
| 
						 | 
					3389f8bd09 | ||
| 
						 | 
					8dc25c527d | ||
| 
						 | 
					46d6bd57c1 | ||
| 
						 | 
					db014fe13d | ||
| 
						 | 
					6c293f4cac | ||
| 
						 | 
					91e5d3736f | ||
| 
						 | 
					e11dee220f | ||
| 
						 | 
					fcebf916d2 | ||
| 
						 | 
					73cc1a7297 | ||
| 
						 | 
					798f112498 | ||
| 
						 | 
					38b5e7dc65 | ||
| 
						 | 
					2799a48f2b | ||
| 
						 | 
					ad5edae6cd | ||
| 
						 | 
					9cb02f0272 | ||
| 
						 | 
					6d24fd9336 | ||
| 
						 | 
					a3a7b78c96 | ||
| 
						 | 
					e995286068 | ||
| 
						 | 
					65fb6d9b7e | ||
| 
						 | 
					eb02d1efad | ||
| 
						 | 
					f8d3e1eefb | ||
| 
						 | 
					218b8fa843 | ||
| 
						 | 
					9f94af6239 | ||
| 
						 | 
					d3584ac40e | ||
| 
						 | 
					90bdb289d0 | ||
| 
						 | 
					78a08750a2 | ||
| 
						 | 
					baba851e97 | ||
| 
						 | 
					2a03783623 | ||
| 
						 | 
					9f2a4438b1 | ||
| 
						 | 
					5ee5287ffa | ||
| 
						 | 
					29547c2c94 | ||
| 
						 | 
					4846c870fa | ||
| 
						 | 
					c17980a032 | ||
| 
						 | 
					a929e419d9 | ||
| 
						 | 
					487d484bae | ||
| 
						 | 
					0ca4c04c61 | ||
| 
						 | 
					c857cf2d67 | ||
| 
						 | 
					acb502028b | ||
| 
						 | 
					533636f3a1 | ||
| 
						 | 
					eb5672901b | ||
| 
						 | 
					53a8716b51 | ||
| 
						 | 
					3aaff612af | ||
| 
						 | 
					fdcd8c6c6a | ||
| 
						 | 
					bafd478604 | ||
| 
						 | 
					987513a88b | ||
| 
						 | 
					a450ab2a3b | ||
| 
						 | 
					db89fe5aad | ||
| 
						 | 
					67a30b92f6 | ||
| 
						 | 
					c397de8c3e | ||
| 
						 | 
					b4db532c45 | ||
| 
						 | 
					ebecc9c80a | ||
| 
						 | 
					4f8556fca8 | ||
| 
						 | 
					68892fb41b | ||
| 
						 | 
					6d6f6c72ac | ||
| 
						 | 
					df5556b945 | ||
| 
						 | 
					d6c74049c3 | ||
| 
						 | 
					18946464a2 | ||
| 
						 | 
					edb5eabee7 | ||
| 
						 | 
					99a305f3e2 | ||
| 
						 | 
					68dc5a6acf | ||
| 
						 | 
					6816461502 | ||
| 
						 | 
					15b93bbd9e | ||
| 
						 | 
					cd61e140f6 | ||
| 
						 | 
					4d861a84e6 | ||
| 
						 | 
					f24de68618 | ||
| 
						 | 
					3bcffff444 | ||
| 
						 | 
					75e9031fa5 | ||
| 
						 | 
					3b77e24399 | ||
| 
						 | 
					0a738e895f | ||
| 
						 | 
					242e5ba035 | ||
| 
						 | 
					c94612106c | ||
| 
						 | 
					320924b4ed | ||
| 
						 | 
					403ecc4521 | ||
| 
						 | 
					6a50b37364 | ||
| 
						 | 
					d9d341ac5d | ||
| 
						 | 
					e9805b731e | ||
| 
						 | 
					c6d4337cd1 | ||
| 
						 | 
					173f4b2ff7 | ||
| 
						 | 
					3b9436264c | ||
| 
						 | 
					35fe87d79d | ||
| 
						 | 
					f1bb7ba9ad | ||
| 
						 | 
					279f229166 | ||
| 
						 | 
					be1794e27b | ||
| 
						 | 
					4d4a2039c8 | ||
| 
						 | 
					3013ae4f35 | ||
| 
						 | 
					bb3f7d3786 | ||
| 
						 | 
					f7cc305e44 | ||
| 
						 | 
					da17f89148 | ||
| 
						 | 
					ec71ab3c6f | ||
| 
						 | 
					0d007f1492 | ||
| 
						 | 
					96f8663b8f | ||
| 
						 | 
					1a4bc1b301 | ||
| 
						 | 
					b51ae58a97 | ||
| 
						 | 
					b126fc32da | ||
| 
						 | 
					b8d234c415 | ||
| 
						 | 
					2c8902d404 | ||
| 
						 | 
					80ad65b196 | ||
| 
						 | 
					744d9ba72b | ||
| 
						 | 
					0c1d708588 | ||
| 
						 | 
					95e79e7c5d | ||
| 
						 | 
					3ce3260d20 | ||
| 
						 | 
					641f4f34d3 | ||
| 
						 | 
					99620cb1c5 | ||
| 
						 | 
					8f5f33f5d2 | ||
| 
						 | 
					78e9230b82 | ||
| 
						 | 
					78aa44c007 | ||
| 
						 | 
					53fd944f00 | ||
| 
						 | 
					9e6cb4ee3d | ||
| 
						 | 
					87ad6f2826 | ||
| 
						 | 
					9050f5a56f | ||
| 
						 | 
					3437004082 | ||
| 
						 | 
					dcf620af87 | ||
| 
						 | 
					128085a02e | ||
| 
						 | 
					302040ec25 | ||
| 
						 | 
					e177c22032 | ||
| 
						 | 
					a11007113a | ||
| 
						 | 
					5e7897bcf4 | ||
| 
						 | 
					9559af3637 | ||
| 
						 | 
					4c499abcdb | ||
| 
						 | 
					0055a503b3 | ||
| 
						 | 
					3a189ee4b6 | ||
| 
						 | 
					e25dc49271 | ||
| 
						 | 
					4208a80db8 | ||
| 
						 | 
					ddb75e0d93 | ||
| 
						 | 
					8b37e992a2 | ||
| 
						 | 
					bac59036cd | ||
| 
						 | 
					6c89a3b77c | ||
| 
						 | 
					dc2ef39fc6 | ||
| 
						 | 
					a4806da2c5 | ||
| 
						 | 
					ee30edb214 | ||
| 
						 | 
					e4ed663fb3 | ||
| 
						 | 
					01629309b0 | ||
| 
						 | 
					059c2991fb | ||
| 
						 | 
					686ec5dd90 | ||
| 
						 | 
					eab9df8ed9 | ||
| 
						 | 
					0107c3d7e2 | ||
| 
						 | 
					2def2f2e2c | ||
| 
						 | 
					44c79892a0 | ||
| 
						 | 
					bc96b314c2 | ||
| 
						 | 
					8dcf749b4e | ||
| 
						 | 
					6a56ec6442 | ||
| 
						 | 
					30e46d7eae | ||
| 
						 | 
					9458b1834b | ||
| 
						 | 
					297f797b97 | ||
| 
						 | 
					c70e80758c | ||
| 
						 | 
					3bf1d7c4f9 | ||
| 
						 | 
					173247041a | ||
| 
						 | 
					3a28772096 | ||
| 
						 | 
					bd08b8aba3 | ||
| 
						 | 
					2ceb0f988b | ||
| 
						 | 
					4ef3b155b8 | ||
| 
						 | 
					350e24cded | ||
| 
						 | 
					1bf8a578bc | ||
| 
						 | 
					4818a101cc | ||
| 
						 | 
					baebf938ef | ||
| 
						 | 
					fea57c7b1e | ||
| 
						 | 
					113dfa68be | ||
| 
						 | 
					60c6514fa1 | ||
| 
						 | 
					114485afc3 | ||
| 
						 | 
					d6a51381b9 | ||
| 
						 | 
					620f13fd7c | ||
| 
						 | 
					6577b2c3d7 | ||
| 
						 | 
					caef522c8b | ||
| 
						 | 
					40ea07de2e | ||
| 
						 | 
					7905e4aa12 | ||
| 
						 | 
					64f4fd708a | ||
| 
						 | 
					b46e4a018f | ||
| 
						 | 
					3e999a9be2 | ||
| 
						 | 
					f656d621e6 | ||
| 
						 | 
					951cc1e6bd | ||
| 
						 | 
					d8d4264f1b | ||
| 
						 | 
					014eeec2b9 | ||
| 
						 | 
					83837bddc3 | ||
| 
						 | 
					f97666db92 | ||
| 
						 | 
					ee08ea41a1 | ||
| 
						 | 
					4ca64610cb | ||
| 
						 | 
					4980145e46 | ||
| 
						 | 
					10cbc19a0c | ||
| 
						 | 
					15fba2b29b | ||
| 
						 | 
					096952f88c | ||
| 
						 | 
					0ea70c1922 | ||
| 
						 | 
					69ac2e2b44 | ||
| 
						 | 
					68098f4d84 | ||
| 
						 | 
					080d52893e | ||
| 
						 | 
					b02334a8d4 | ||
| 
						 | 
					27118add22 | ||
| 
						 | 
					2a6f98a1e8 | ||
| 
						 | 
					1f67f2fdee | ||
| 
						 | 
					ebf4d294a8 | ||
| 
						 | 
					4a4dbacc95 | ||
| 
						 | 
					687839b5f8 | ||
| 
						 | 
					8fb339034f | ||
| 
						 | 
					8e9fd9c985 | ||
| 
						 | 
					72400f71c0 | ||
| 
						 | 
					1151587951 | ||
| 
						 | 
					abcd500045 | ||
| 
						 | 
					beda24e736 | ||
| 
						 | 
					37b2c5c2df | ||
| 
						 | 
					7b5246ebf1 | ||
| 
						 | 
					d6d5e72f48 | ||
| 
						 | 
					fa8e88d489 | ||
| 
						 | 
					1ef2da9f76 | ||
| 
						 | 
					1ebd894be7 | ||
| 
						 | 
					a9c493d105 | ||
| 
						 | 
					f833d73fab | ||
| 
						 | 
					9e6602f114 | ||
| 
						 | 
					3bdfef9f8b | ||
| 
						 | 
					6f7f475a6b | ||
| 
						 | 
					8fc5fab67b | ||
| 
						 | 
					6927e92396 | ||
| 
						 | 
					c7470396d7 | ||
| 
						 | 
					f21570e2e4 | ||
| 
						 | 
					51f406e20c | ||
| 
						 | 
					9e3fde744e | ||
| 
						 | 
					ccf406ae68 | ||
| 
						 | 
					bc78d1e079 | ||
| 
						 | 
					d151eb261e | ||
| 
						 | 
					0856598cd9 | ||
| 
						 | 
					f0563efc62 | ||
| 
						 | 
					84dfa9a8a5 | ||
| 
						 | 
					8e25489cca | ||
| 
						 | 
					198f95e1ca | ||
| 
						 | 
					7e02fe89ea | ||
| 
						 | 
					819356412c | ||
| 
						 | 
					deb789bc1b | ||
| 
						 | 
					133ba74548 | ||
| 
						 | 
					1461e32643 | ||
| 
						 | 
					f400c3d9ac | ||
| 
						 | 
					7e595a4f74 | ||
| 
						 | 
					18c9c499b2 | ||
| 
						 | 
					24ae115ed4 | ||
| 
						 | 
					7f345558cd | ||
| 
						 | 
					57177cc910 | ||
| 
						 | 
					cea258bc21 | ||
| 
						 | 
					ed9b1c8ba7 | ||
| 
						 | 
					5a79fd89e9 | ||
| 
						 | 
					42a130db08 | ||
| 
						 | 
					320a8d19de | ||
| 
						 | 
					5721506007 | ||
| 
						 | 
					803e8cb2f4 | ||
| 
						 | 
					98492fd0c0 | ||
| 
						 | 
					0b07178577 | ||
| 
						 | 
					07e545079c | ||
| 
						 | 
					95d64dc5e8 | ||
| 
						 | 
					abe546dcda | ||
| 
						 | 
					e6f367acaf | ||
| 
						 | 
					a9b61853b9 | ||
| 
						 | 
					5afc04a630 | ||
| 
						 | 
					1da4cc2782 | ||
| 
						 | 
					c5ebc89e4f | ||
| 
						 | 
					dfc1719cce | ||
| 
						 | 
					0812259470 | ||
| 
						 | 
					e1476c5840 | ||
| 
						 | 
					e30ea28e3f | ||
| 
						 | 
					4a6d3aab7f | ||
| 
						 | 
					8157146498 | ||
| 
						 | 
					94d23888b1 | ||
| 
						 | 
					737fe9bb4a | ||
| 
						 | 
					0051ed2e73 | ||
| 
						 | 
					e0595957e2 | ||
| 
						 | 
					8d09ff7fdb | ||
| 
						 | 
					04feb66b07 | ||
| 
						 | 
					54b2ac7f24 | ||
| 
						 | 
					12356a35fa | ||
| 
						 | 
					12262304ac | ||
| 
						 | 
					c58f97452e | ||
| 
						 | 
					eb3872f7a6 | ||
| 
						 | 
					9fa178d513 | ||
| 
						 | 
					043b184065 | ||
| 
						 | 
					10559bb894 | ||
| 
						 | 
					d0000d66b2 | ||
| 
						 | 
					b447ac738a | ||
| 
						 | 
					faebfc238c | ||
| 
						 | 
					c28fbd37cc | ||
| 
						 | 
					4b8396959d | ||
| 
						 | 
					b39d510e07 | ||
| 
						 | 
					286dda7f80 | ||
| 
						 | 
					7bda896e2d | ||
| 
						 | 
					ba4feeea87 | ||
| 
						 | 
					6f52eae3c6 | ||
| 
						 | 
					40ea8d56e6 | ||
| 
						 | 
					72e562e8a8 | ||
| 
						 | 
					6fa01bfe19 | ||
| 
						 | 
					0ef59c9b91 | ||
| 
						 | 
					d768d2232b | ||
| 
						 | 
					b44a200731 | ||
| 
						 | 
					016815e0d1 | ||
| 
						 | 
					590534e4a6 | ||
| 
						 | 
					7ea9d4e519 | ||
| 
						 | 
					e0ab09f533 | ||
| 
						 | 
					fbe98f1b16 | ||
| 
						 | 
					d0675b8443 | ||
| 
						 | 
					3ea1ed02ae | ||
| 
						 | 
					ba120b1e0b | ||
| 
						 | 
					acf6995c2d | ||
| 
						 | 
					8306860f90 | ||
| 
						 | 
					65974166be | ||
| 
						 | 
					ee8924f986 | ||
| 
						 | 
					170e575465 | ||
| 
						 | 
					b7d5317b10 | ||
| 
						 | 
					f12e7748c5 | ||
| 
						 | 
					69a2418afc | ||
| 
						 | 
					4924ddd172 | ||
| 
						 | 
					1889b43786 | ||
| 
						 | 
					f2e38a4203 | ||
| 
						 | 
					90a8fac8d4 | ||
| 
						 | 
					04402c5ab9 | ||
| 
						 | 
					f8f710df99 | ||
| 
						 | 
					b8105bb6fb | ||
| 
						 | 
					1d18c898b2 | ||
| 
						 | 
					95e208000f | ||
| 
						 | 
					ecdddef81d | ||
| 
						 | 
					c9b1d329e6 | ||
| 
						 | 
					e68c16c7a4 | ||
| 
						 | 
					585c57fe3a | ||
| 
						 | 
					d04cbac79c | ||
| 
						 | 
					044585ee9b | ||
| 
						 | 
					299478e840 | ||
| 
						 | 
					b2d69be5f8 | ||
| 
						 | 
					dc970bbf3c | ||
| 
						 | 
					8717bd5d5d | ||
| 
						 | 
					5b307a8407 | ||
| 
						 | 
					daef66087d | ||
| 
						 | 
					1ad1cf4460 | ||
| 
						 | 
					c0b9718368 | ||
| 
						 | 
					d684f323b8 | ||
| 
						 | 
					24a1c56fe6 | ||
| 
						 | 
					cdeba4f84e | ||
| 
						 | 
					cafba196cf | ||
| 
						 | 
					493b1b12b3 | ||
| 
						 | 
					5320f88230 | ||
| 
						 | 
					246ec2c3ac | ||
| 
						 | 
					9c9b45aeab | ||
| 
						 | 
					8c5dc43735 | ||
| 
						 | 
					b1e812314f | ||
| 
						 | 
					c14f47a74b | ||
| 
						 | 
					58a5b4a5e5 | ||
| 
						 | 
					1cfc2bf36f | ||
| 
						 | 
					5a56d826d9 | 
							
								
								
									
										73
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,73 @@
 | 
			
		||||
# Introduction
 | 
			
		||||
 | 
			
		||||
### Hey you !
 | 
			
		||||
 | 
			
		||||
Thank you for wanting to help. Even the smallest things can help this project become better.
 | 
			
		||||
 | 
			
		||||
Please read the guidelines before contributing, and follow them (or try to) when contributing.
 | 
			
		||||
 | 
			
		||||
### What you can do to help.
 | 
			
		||||
 | 
			
		||||
There are many ways to contribute to this project, you could [translate the app](https://crowdin.com/project/readerforselfoss), report bugs, request missing features, suggest enhancements and changes to existing ones. You also can improve the README with useful tips that could help the other users.
 | 
			
		||||
 | 
			
		||||
You can fork the repository, and [help me solve some issues](https://github.com/aminecmi/ReaderforSelfoss/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs%22) or [develop new things](https://github.com/aminecmi/ReaderforSelfoss/issues)
 | 
			
		||||
 | 
			
		||||
### What I can't help you with.
 | 
			
		||||
 | 
			
		||||
Please, don't use the issue tracker for anything related to [Selfoss itself](https://github.com/SSilence/selfoss). The app calls the api provided by Selfoss, and can't help with solving issues with your Selfoss instance.
 | 
			
		||||
 | 
			
		||||
Always check if the web version of your instance is working.
 | 
			
		||||
 | 
			
		||||
# Some rules
 | 
			
		||||
### Bug reports/Feature request
 | 
			
		||||
 | 
			
		||||
* Always search before reporting an issue or asking for a feature to avoid duplicates.
 | 
			
		||||
* Include your unique user id. It's displayed on the debug settings page. (You can tap it, it'll be copied to your clipboard)
 | 
			
		||||
* Include every other useful details (app version, phone model, Android version and screenshots when possible).
 | 
			
		||||
* Avoid bumping non-fatal issues, or feature requests. I'll try to fix them as soon as possible, and try to prioritize the requests. (You may wan to use the [reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) for that)
 | 
			
		||||
 | 
			
		||||
### Pull requests
 | 
			
		||||
 | 
			
		||||
* Don't create a PR for translations. See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654) for an explanation why.
 | 
			
		||||
* Please ask before starting to work on an issue. I may be working on it, or someone else could be doing so.
 | 
			
		||||
* Each pull request should implement **ONE** feature or bugfix. Keep in mind that you can submit as many PR as you want.
 | 
			
		||||
* Your code must be simple and clear enough to avoid using comments to explain what it does.
 | 
			
		||||
* Follow the used coding style [the android koding style](https://android.github.io/kotlin-guides/style.html) ([some idoms for reference](http://kotlinlang.org/docs/reference/idioms.html)) with more to come.
 | 
			
		||||
* Try as much as possible to write a test for your feature, and if you do so, run it, and make it work.
 | 
			
		||||
* Always check your changes and discard the ones that are irrelevant to your feature or bugfix.
 | 
			
		||||
* Have meaningful commit messages.
 | 
			
		||||
* Always reference the issue you are working on in your PR description.
 | 
			
		||||
* Be willing to accept criticism on your PRs (as I am on mine).
 | 
			
		||||
* Remember that PR review can take time.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Install Selfoss (if you don't have an instance)
 | 
			
		||||
 | 
			
		||||
I won't provide any selfoss instance url. If you want to help, but to not have one, you'll have to install one, and use it.
 | 
			
		||||
 | 
			
		||||
All the details to need are [here](https://selfoss.aditu.de/).
 | 
			
		||||
 | 
			
		||||
# Build the project
 | 
			
		||||
 | 
			
		||||
You can directly import this project into IntellIJ/Android Studio.
 | 
			
		||||
 | 
			
		||||
You'll have to:
 | 
			
		||||
 | 
			
		||||
- Define some parameters either in `~/.gradle/gradle.properties` or as gradle parameters (see the examples)
 | 
			
		||||
 | 
			
		||||
    - appLoginUrl, appLoginUsername and appLoginPassword: url, username and password of a selfoss instance. **These are only used for tests. They can be empty if you don't test API calls.**
 | 
			
		||||
 | 
			
		||||
### Examples:
 | 
			
		||||
#### Inside ~/.gradle/gradle.properties
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
appLoginUrl="URL" # It can be empty.
 | 
			
		||||
appLoginUsername="LOGIN" # It can be empty.
 | 
			
		||||
appLoginPassword="PASS" # It can be empty.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### As gradle parameters
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./gradlew .... -P appLoginUrl="URL" -P appLoginUsername="LOGIN" -P appLoginPassword="PASS"
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
### Prerequisites
 | 
			
		||||
 | 
			
		||||
* [ ] Are you running the latest version?
 | 
			
		||||
* [ ] Did you check for an existing issue ?
 | 
			
		||||
* [ ] Are you reporting to the correct repository?
 | 
			
		||||
* [ ] Did you perform a cursory search?
 | 
			
		||||
* [ ] Did you read the `CONTRIBUTING` guide ?
 | 
			
		||||
 | 
			
		||||
### Description
 | 
			
		||||
 | 
			
		||||
[Description of the bug or feature]
 | 
			
		||||
 | 
			
		||||
### Steps to Reproduce
 | 
			
		||||
 | 
			
		||||
1. [First Step]
 | 
			
		||||
2. [Second Step]
 | 
			
		||||
3. [and so on...]
 | 
			
		||||
 | 
			
		||||
**Expected behavior:** [What you expected to happen]
 | 
			
		||||
 | 
			
		||||
**Actual behavior:** [What actually happened]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Screenshots (optional)
 | 
			
		||||
 | 
			
		||||
`...`
 | 
			
		||||
 | 
			
		||||
### Device
 | 
			
		||||
 | 
			
		||||
- Device (manufacturer, model ...)
 | 
			
		||||
- OS (Android Version, ROM/Stock, Rooted/not, mods...)
 | 
			
		||||
- App version _(See Prerequisites)_
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
## Types of changes
 | 
			
		||||
 | 
			
		||||
- [ ] I have read the **CONTRIBUTING** document.
 | 
			
		||||
- [ ] My code follows the code style of this project.
 | 
			
		||||
- [ ] I have updated the documentation accordingly.
 | 
			
		||||
- [ ] I have added tests to cover my changes.
 | 
			
		||||
- [ ] All new and existing tests passed.
 | 
			
		||||
- [ ] This is **NOT** translation related. (See [here](https://github.com/aminecmi/ReaderforSelfoss/pull/170#issuecomment-355715654))
 | 
			
		||||
 | 
			
		||||
This closes issue #XXX
 | 
			
		||||
 | 
			
		||||
This is implements feature #YYY
 | 
			
		||||
 | 
			
		||||
This finishes chore #ZZZ
 | 
			
		||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -214,7 +214,6 @@ gradle-app.setting
 | 
			
		||||
 | 
			
		||||
# End of https://www.gitignore.io/api/java,gradle,android,androidstudio
 | 
			
		||||
 | 
			
		||||
secrets.xml
 | 
			
		||||
release/
 | 
			
		||||
 | 
			
		||||
mipmap-*
 | 
			
		||||
release/
 | 
			
		||||
crowdin.properties
 | 
			
		||||
							
								
								
									
										368
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,3 +1,369 @@
 | 
			
		||||
**1.7.x**
 | 
			
		||||
 | 
			
		||||
- Added experimental issue to set a default timeout. Should work for #238.
 | 
			
		||||
 | 
			
		||||
- Closing #220.
 | 
			
		||||
 | 
			
		||||
- Start of #238. "Add a quick shortcut to open the app on offline mode ?"
 | 
			
		||||
 | 
			
		||||
- Closes #216. Issue with selfoss version 2.19.
 | 
			
		||||
 | 
			
		||||
- Closes #179. Sync of read/unread/star/unstar items on background task or on app reload with network available.
 | 
			
		||||
 | 
			
		||||
- Closes #33. Background sync with settings.
 | 
			
		||||
 | 
			
		||||
- Closing #1. Initial article caching.
 | 
			
		||||
 | 
			
		||||
- Closing #228 by removing the list action bar. Action buttons are exclusively on the card view from now on.
 | 
			
		||||
 | 
			
		||||
- Closing #38. Only doing api calls on network available.
 | 
			
		||||
 | 
			
		||||
**1.6.x**
 | 
			
		||||
 | 
			
		||||
- Handling hidden tags.
 | 
			
		||||
 | 
			
		||||
- Fixed pre-lolipop issue with automatic theme changes.
 | 
			
		||||
 | 
			
		||||
- Removed all Build config things.
 | 
			
		||||
 | 
			
		||||
- Removed firebase and fabric.
 | 
			
		||||
 | 
			
		||||
- Added Acra for optional crash reporting and error logging.
 | 
			
		||||
 | 
			
		||||
- Dynamic themes !
 | 
			
		||||
 | 
			
		||||
- Strings cleaning.
 | 
			
		||||
 | 
			
		||||
- Versions updates.
 | 
			
		||||
 | 
			
		||||
- Fixes #215, #208.
 | 
			
		||||
 | 
			
		||||
**1.5.7.x**
 | 
			
		||||
 | 
			
		||||
- Added confirmation to the mark as read and update menues.
 | 
			
		||||
 | 
			
		||||
- Add to favorites from article viewer.
 | 
			
		||||
 | 
			
		||||
- Added an option to use a webview in the article viewer (see #149)
 | 
			
		||||
 | 
			
		||||
- Fixes (#151 #152 #155 #157 #160 #174) and more.
 | 
			
		||||
 | 
			
		||||
- New year fixes !!!
 | 
			
		||||
 | 
			
		||||
- Changed page indicator position as it was overlaping content.
 | 
			
		||||
 | 
			
		||||
- Now using slack instead of gitter.
 | 
			
		||||
 | 
			
		||||
- Moved completely to a webview to fix #161.
 | 
			
		||||
 | 
			
		||||
- Fixed typos in French ( Thanks @aancel )
 | 
			
		||||
 | 
			
		||||
- Updated the Contribution guide about translations.
 | 
			
		||||
 | 
			
		||||
- Better handling for articles update. (See #169)
 | 
			
		||||
 | 
			
		||||
- Ability to change the article viewer content font size (see #153)
 | 
			
		||||
 | 
			
		||||
- Versions updates * 2.
 | 
			
		||||
 | 
			
		||||
- Added padding to the recyclerview.
 | 
			
		||||
 | 
			
		||||
**1.5.5.x (didn't last long) AND 1.5.6.x**
 | 
			
		||||
 | 
			
		||||
- Toolbar in reader activity.
 | 
			
		||||
 | 
			
		||||
- Marking items as read on scroll (with settings to enable/disable).
 | 
			
		||||
 | 
			
		||||
- Swapped the title and subtitle in the article viewer.
 | 
			
		||||
 | 
			
		||||
- Added an animation to the viewpager.
 | 
			
		||||
 | 
			
		||||
- Completed Dutch, Indonesian and Portuguese translations !
 | 
			
		||||
 | 
			
		||||
- Fixed #142, #144, #147.
 | 
			
		||||
 | 
			
		||||
- Changed versions handling.
 | 
			
		||||
 | 
			
		||||
- Removed indonesian english as it was causing issues with the english version of the app.
 | 
			
		||||
 | 
			
		||||
**1.5.4.22**
 | 
			
		||||
 | 
			
		||||
- You can now scroll through the loaded articles !
 | 
			
		||||
 | 
			
		||||
**1.5.4.21**
 | 
			
		||||
 | 
			
		||||
- Spanish translation and some Indonesian !
 | 
			
		||||
 | 
			
		||||
**1.5.4.20**
 | 
			
		||||
 | 
			
		||||
- Turkish translation !
 | 
			
		||||
 | 
			
		||||
**1.5.4.19**
 | 
			
		||||
 | 
			
		||||
- Fixed an issue with crowdin configuration (and its translations)
 | 
			
		||||
 | 
			
		||||
**1.5.4.18**
 | 
			
		||||
 | 
			
		||||
- Typo fix.
 | 
			
		||||
 | 
			
		||||
- The real last infinite scroll bug fix.
 | 
			
		||||
 | 
			
		||||
- Simplified Chinese translation !
 | 
			
		||||
 | 
			
		||||
**1.5.4.17**
 | 
			
		||||
 | 
			
		||||
- Fixed the last bug with infinite scroll.
 | 
			
		||||
 | 
			
		||||
**1.5.4.16**
 | 
			
		||||
 | 
			
		||||
- Fixing list view displaying issues.
 | 
			
		||||
 | 
			
		||||
- Endless scroll is not in beta anymore.
 | 
			
		||||
 | 
			
		||||
**1.5.4.15**
 | 
			
		||||
 | 
			
		||||
- Fixed an issue with the sources list.
 | 
			
		||||
 | 
			
		||||
**1.5.4.14**
 | 
			
		||||
 | 
			
		||||
- Fixing infinite scroll trying to load more items when there are no more.
 | 
			
		||||
 | 
			
		||||
**1.5.4.13**
 | 
			
		||||
 | 
			
		||||
- Displaying the right number of items.
 | 
			
		||||
 | 
			
		||||
- Fixing infinite scroll remaining issues. Should be stable enough.
 | 
			
		||||
 | 
			
		||||
**1.5.4.12**
 | 
			
		||||
 | 
			
		||||
- Fixed fab and toolbar issue (#113)
 | 
			
		||||
 | 
			
		||||
- Fixed links clickable (#114)
 | 
			
		||||
 | 
			
		||||
- Changed the link colors in the article viewer
 | 
			
		||||
 | 
			
		||||
**1.5.4.11**
 | 
			
		||||
 | 
			
		||||
- Hiding FABs on scroll.
 | 
			
		||||
 | 
			
		||||
- Closing #109 (code cleaning)
 | 
			
		||||
 | 
			
		||||
- Hiding fabs on scroll (#101)
 | 
			
		||||
 | 
			
		||||
**1.5.4.10**
 | 
			
		||||
 | 
			
		||||
- Displaying a loader when "reading more" in the article viewer.
 | 
			
		||||
 | 
			
		||||
- Displaying the thumbnail instead of icon on the article viewer.
 | 
			
		||||
 | 
			
		||||
- Scrolling to top when loading content with the "read more" button.
 | 
			
		||||
 | 
			
		||||
**1.5.4.09**
 | 
			
		||||
 | 
			
		||||
- Using the kotlin wrapper for the material drawer (see #98 for more details).
 | 
			
		||||
 | 
			
		||||
- Updated support libraries
 | 
			
		||||
 | 
			
		||||
- Changed the Floating Action Button to the support library version.
 | 
			
		||||
 | 
			
		||||
- New reader activity action bar #103.
 | 
			
		||||
 | 
			
		||||
**1.5.4.08**
 | 
			
		||||
 | 
			
		||||
- Thanks @jrafaelsantana for translating the whole app in Brazilian Portuguese.
 | 
			
		||||
 | 
			
		||||
**1.5.4.07**
 | 
			
		||||
 | 
			
		||||
- Loading more items on swipe too.
 | 
			
		||||
 | 
			
		||||
- Fixed popup menu style. User may need to reselect the theme.
 | 
			
		||||
 | 
			
		||||
- Disabled reporting marking items as read if there isn't an issue.
 | 
			
		||||
 | 
			
		||||
**1.5.4.05/06**
 | 
			
		||||
 | 
			
		||||
- Translation fix.
 | 
			
		||||
 | 
			
		||||
**1.5.4.04**
 | 
			
		||||
 | 
			
		||||
- Fixing an issue with marking items as read (something related to an old version of selfoss).
 | 
			
		||||
 | 
			
		||||
**1.5.4.03**
 | 
			
		||||
 | 
			
		||||
- Trying to fix some issue with pre-launch reports. Reverted because it seems to be related to the dev console side.
 | 
			
		||||
 | 
			
		||||
**1.5.4.02**
 | 
			
		||||
 | 
			
		||||
- Fixing full height cards issue.
 | 
			
		||||
 | 
			
		||||
**1.5.4.01**
 | 
			
		||||
 | 
			
		||||
- Removed the "apk downloaded from outside of playstore" message.
 | 
			
		||||
 | 
			
		||||
- Versions update.
 | 
			
		||||
 | 
			
		||||
- HTML viewer version update. It should fix an issue with images.
 | 
			
		||||
 | 
			
		||||
- Some code cleaning.
 | 
			
		||||
 | 
			
		||||
**1.5.4.00**
 | 
			
		||||
 | 
			
		||||
- Added issue reporting from within the app.
 | 
			
		||||
 | 
			
		||||
**1.5.3.06**
 | 
			
		||||
 | 
			
		||||
- Fixed infinite scroll not working.
 | 
			
		||||
 | 
			
		||||
- Fixed logs not working.
 | 
			
		||||
 | 
			
		||||
- Temporary workaround handling opening invalid urls. Waiting to solve #83.
 | 
			
		||||
 | 
			
		||||
**1.5.3.05**
 | 
			
		||||
 | 
			
		||||
- Fixed an issue on older versions of Android.
 | 
			
		||||
 | 
			
		||||
- Libs update.
 | 
			
		||||
 | 
			
		||||
**1.5.3.04**
 | 
			
		||||
 | 
			
		||||
- Crowdin translations
 | 
			
		||||
 | 
			
		||||
**1.5.3.03**
 | 
			
		||||
 | 
			
		||||
- Libs updates.
 | 
			
		||||
 | 
			
		||||
- Translation fix.
 | 
			
		||||
 | 
			
		||||
**1.5.3.01/02**
 | 
			
		||||
 | 
			
		||||
- Added translation link to the settings page.
 | 
			
		||||
 | 
			
		||||
- Added the translation link to the README.
 | 
			
		||||
 | 
			
		||||
**1.5.3.00**
 | 
			
		||||
 | 
			
		||||
- (BETA) Added pull from bottom to load more pages of results. May be buggy.
 | 
			
		||||
 | 
			
		||||
**1.5.2.18/19**
 | 
			
		||||
 | 
			
		||||
- APK minification finally working. That means less space taken !
 | 
			
		||||
- Added an option to log every API call.
 | 
			
		||||
 | 
			
		||||
**1.5.2.17**
 | 
			
		||||
 | 
			
		||||
- Source code and tracker links weren't being set, and updated the contributing doc.
 | 
			
		||||
 | 
			
		||||
**1.5.2.15/16**
 | 
			
		||||
 | 
			
		||||
- Adding an account header on the lateral drawer.
 | 
			
		||||
 | 
			
		||||
- The account header is only displayed when the setting is enabled.
 | 
			
		||||
 | 
			
		||||
**1.5.2.13/14**
 | 
			
		||||
 | 
			
		||||
- Updated glide.
 | 
			
		||||
 | 
			
		||||
- Loading images from self signed certificate now working.
 | 
			
		||||
 | 
			
		||||
**1.5.2.12**
 | 
			
		||||
 | 
			
		||||
- Self signed certificates are now working for loading data. Image are not loading yet.
 | 
			
		||||
 | 
			
		||||
**1.5.2.11**
 | 
			
		||||
 | 
			
		||||
- Added a random unique identifier to be used in the logs.
 | 
			
		||||
 | 
			
		||||
**1.5.2.08/09/10**
 | 
			
		||||
 | 
			
		||||
- Added settable logs for reading articles problems.
 | 
			
		||||
 | 
			
		||||
**1.5.2.07**
 | 
			
		||||
 | 
			
		||||
- Added the ability to choose the number of items loaded (the maximum value is 200 and is imposed by the selfoss api)
 | 
			
		||||
 | 
			
		||||
**1.5.2.06**
 | 
			
		||||
 | 
			
		||||
- Fix problem introduced in 1.5.2.04. SVG file not working on older versions of android.
 | 
			
		||||
 | 
			
		||||
**1.5.2.05**
 | 
			
		||||
 | 
			
		||||
- Versions updates
 | 
			
		||||
 | 
			
		||||
**1.5.2.04**
 | 
			
		||||
 | 
			
		||||
- Reverted to the old icon.
 | 
			
		||||
 | 
			
		||||
- Better icon for the intro activity.
 | 
			
		||||
 | 
			
		||||
- Updated gradle version.
 | 
			
		||||
 | 
			
		||||
**1.5.2.03**
 | 
			
		||||
 | 
			
		||||
- Added the ability to accept self signed certificates. (Needs more testing)
 | 
			
		||||
 | 
			
		||||
**1.5.2.02**
 | 
			
		||||
 | 
			
		||||
- Added optional login option.
 | 
			
		||||
 | 
			
		||||
**1.5.2.01**
 | 
			
		||||
 | 
			
		||||
- New (Better) Icon !
 | 
			
		||||
 | 
			
		||||
**1.5.2.0**
 | 
			
		||||
 | 
			
		||||
- New Icon !
 | 
			
		||||
 | 
			
		||||
**1.5.1.9/10/11**
 | 
			
		||||
 | 
			
		||||
- Hiding the unread badge when marking all items as read.
 | 
			
		||||
 | 
			
		||||
**1.5.1.8**
 | 
			
		||||
 | 
			
		||||
- Fixes and libs updates.
 | 
			
		||||
 | 
			
		||||
**1.5.1.7**
 | 
			
		||||
 | 
			
		||||
- Bug fixes.
 | 
			
		||||
 | 
			
		||||
- Code cleaning
 | 
			
		||||
 | 
			
		||||
**1.5.1.6**
 | 
			
		||||
 | 
			
		||||
- Added back the badges after it was fixed on the library side.
 | 
			
		||||
 | 
			
		||||
**1.5.1.5**
 | 
			
		||||
 | 
			
		||||
- THEMES !!!! For now, the app has predefined themes. You can ask for new ones until I make them dynamic.
 | 
			
		||||
 | 
			
		||||
**1.5.1.3/4**
 | 
			
		||||
 | 
			
		||||
- Fixes introduces by the previous alpha (1.5.1.2)
 | 
			
		||||
 | 
			
		||||
**1.5.1.2**
 | 
			
		||||
 | 
			
		||||
- Added testing to the CI.
 | 
			
		||||
 | 
			
		||||
- Code cleaning
 | 
			
		||||
 | 
			
		||||
- Display the pull to refresh loader on api call
 | 
			
		||||
 | 
			
		||||
- Fixes :
 | 
			
		||||
 | 
			
		||||
  - Can't pull down to refresh on first launch
 | 
			
		||||
 | 
			
		||||
  - Recurring crash because of the url
 | 
			
		||||
 | 
			
		||||
  - Couldn't open some urls because of missing "http"
 | 
			
		||||
 | 
			
		||||
  - Adding a source with invalid url would crash
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
**1.5.1.1**
 | 
			
		||||
 | 
			
		||||
- Fixed an issue when trying to add a source without being logged in.
 | 
			
		||||
 | 
			
		||||
- Reloading drawer tags badges on slide to refresh.
 | 
			
		||||
 | 
			
		||||
**1.5.1**
 | 
			
		||||
 | 
			
		||||
- Added a drawer for filtering sources and tags.
 | 
			
		||||
@@ -159,4 +525,4 @@ _Updates_
 | 
			
		||||
 | 
			
		||||
**1.3.3.4**
 | 
			
		||||
 | 
			
		||||
...
 | 
			
		||||
...
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,30 +1,33 @@
 | 
			
		||||
# ReaderForSelfoss
 | 
			
		||||
 | 
			
		||||
[](https://circleci.com/gh/aminecmi/ReaderforSelfoss/tree/master)
 | 
			
		||||
 | 
			
		||||
This is the repo of [Reader For Selfoss](https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss&hl=en).
 | 
			
		||||
[](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ) [](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/) [](https://www.codetriage.com/aminecmi/readerforselfoss) [](https://crowdin.com/project/readerforselfoss)
 | 
			
		||||
 | 
			
		||||
It's an RSS Reader for Android, that **only** works with [Selfoss](https://selfoss.aditu.de/)
 | 
			
		||||
 | 
			
		||||
<a href='https://play.google.com/store/apps/details?id=apps.amine.bou.readerforselfoss'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="100"/></a> <a href="https://f-droid.org/packages/apps.amine.bou.readerforselfoss"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
 | 
			
		||||
 | 
			
		||||
## Build
 | 
			
		||||
Also, the last APK built from source is available [here](https://jenkins.amine-bou.fr/job/ReaderForSelfoss/lastSuccessfulBuild/artifact/SignApksBuilder-out/selfoss-key/selfoss/app-githubConfig-release-unsigned.apk/app-githubConfig-release.apk).
 | 
			
		||||
 | 
			
		||||
You can directly import this project into IntellIJ/Android Studio.
 | 
			
		||||
## Join the alpha channel
 | 
			
		||||
 | 
			
		||||
You'll have to:
 | 
			
		||||
**Keep in mind, it could be instable, but you'll have the new updates faster**
 | 
			
		||||
 | 
			
		||||
- [Create your own launcher icon](https://developer.android.com/studio/write/image-asset-studio.html#creating-launcher)
 | 
			
		||||
- First, join the google [group](https://groups.google.com/d/forum/reader-for-selfoss-alpha-testing).
 | 
			
		||||
- Then, join the [alpha channel](https://play.google.com/apps/testing/apps.amine.bou.readerforselfoss) of the app.
 | 
			
		||||
- You'll be able to update the app for the current alpha version.
 | 
			
		||||
 | 
			
		||||
- Configure Fabric, or [remove it](https://docs.fabric.io/android/fabric/settings/removing.html#).
 | 
			
		||||
- Define the following in `res/values/strings.xml` or create `res/values/secrets.xml`
 | 
			
		||||
## Want to help ?
 | 
			
		||||
 | 
			
		||||
    - mercury: A [Mercury](https://mercury.postlight.com/web-parser/) web parser api key for the internal browser
 | 
			
		||||
    - feedback_email: An email to receive users  feedback.
 | 
			
		||||
    - source_url: an url to the source code, used in the settings
 | 
			
		||||
    - tracker_url: an url to the tracker, used in the settings
 | 
			
		||||
1. **You'll have to have a Selfoss instance running.** You'll find everything you need to install it [here](https://selfoss.aditu.de/).
 | 
			
		||||
 | 
			
		||||
2. Check the [Contribution guide](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md).
 | 
			
		||||
 | 
			
		||||
3. Build the project by following [these steps](https://github.com/aminecmi/ReaderforSelfoss/blob/master/.github/CONTRIBUTING.md#build-the-project) (you should have read them after the contribution guide)
 | 
			
		||||
 | 
			
		||||
## Useful links
 | 
			
		||||
 | 
			
		||||
- [Check what changed](https://github.com/aminecmi/ReaderforSelfoss/blob/master/CHANGELOG.md)
 | 
			
		||||
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
 | 
			
		||||
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
 | 
			
		||||
- [Help translation the app](https://crowdin.com/project/readerforselfoss)
 | 
			
		||||
- [Ask for help](https://join.slack.com/t/readerforselfoss/shared_invite/enQtMjkyNzc3NjM2Mjc1LTUzZTZhOGM5YjQ1MTI5MWZiODRjMjE1ZDBmMzQxZmQ3NWZhYTNhMTBjNGEwNmE2ZGFjODU5NjUxZjBkMWJmMDQ)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										185
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						@@ -1,32 +1,50 @@
 | 
			
		||||
buildscript {
 | 
			
		||||
    repositories {
 | 
			
		||||
        maven { url 'https://maven.fabric.io/public' }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath 'io.fabric.tools:gradle:1.+'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def gitVersion() {
 | 
			
		||||
    def process
 | 
			
		||||
    def maybeTagOfCurrentCommit = 'git describe --contains HEAD'.execute()
 | 
			
		||||
    if (maybeTagOfCurrentCommit.text.isEmpty()) {
 | 
			
		||||
        println "No tag on current commit. Will take the latest one."
 | 
			
		||||
        process = "git for-each-ref refs/tags --sort=-authordate --format='%(refname:short)' --count=1".execute()
 | 
			
		||||
    } else {
 | 
			
		||||
        println "Tag found on current commit"
 | 
			
		||||
        process = 'git describe --contains HEAD'.execute()
 | 
			
		||||
    }
 | 
			
		||||
    return process.text.replaceAll("'", "").substring(1).replaceAll("\\.", "").trim()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def versionCodeFromGit() {
 | 
			
		||||
    println "version code " + gitVersion()
 | 
			
		||||
    return gitVersion().toInteger()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def versionNameFromGit() {
 | 
			
		||||
    println "version name " + gitVersion()
 | 
			
		||||
    return gitVersion()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply plugin: 'kotlin-kapt'
 | 
			
		||||
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
 | 
			
		||||
apply plugin: 'io.fabric'
 | 
			
		||||
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
    maven { url 'https://maven.fabric.io/public' }
 | 
			
		||||
}
 | 
			
		||||
apply plugin: 'kotlin-android-extensions'
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion 25
 | 
			
		||||
    buildToolsVersion "25.0.3"
 | 
			
		||||
    compileOptions {
 | 
			
		||||
        sourceCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
        targetCompatibility JavaVersion.VERSION_1_8
 | 
			
		||||
    }
 | 
			
		||||
    compileSdkVersion 28
 | 
			
		||||
    buildToolsVersion '28.0.3'
 | 
			
		||||
    defaultConfig {
 | 
			
		||||
        applicationId "apps.amine.bou.readerforselfoss"
 | 
			
		||||
        minSdkVersion 16
 | 
			
		||||
        targetSdkVersion 25
 | 
			
		||||
        versionCode 1511
 | 
			
		||||
        versionName "1.5.1.1"
 | 
			
		||||
        targetSdkVersion 28
 | 
			
		||||
        versionCode versionCodeFromGit()
 | 
			
		||||
        versionName versionNameFromGit()
 | 
			
		||||
 | 
			
		||||
        // Enabling multidex support.
 | 
			
		||||
        multiDexEnabled true
 | 
			
		||||
@@ -35,106 +53,129 @@ android {
 | 
			
		||||
            disable 'InvalidPackage'
 | 
			
		||||
        }
 | 
			
		||||
        vectorDrawables.useSupportLibrary = true
 | 
			
		||||
 | 
			
		||||
        // tests
 | 
			
		||||
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 | 
			
		||||
 | 
			
		||||
        javaCompileOptions {
 | 
			
		||||
            annotationProcessorOptions {
 | 
			
		||||
                arguments = ["room.schemaLocation":
 | 
			
		||||
                                     "$projectDir/schemas".toString()]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    buildTypes {
 | 
			
		||||
        release {
 | 
			
		||||
            minifyEnabled false
 | 
			
		||||
            minifyEnabled true
 | 
			
		||||
            shrinkResources true
 | 
			
		||||
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
 | 
			
		||||
                    'proguard-rules.pro'
 | 
			
		||||
        }
 | 
			
		||||
        debug {
 | 
			
		||||
            buildConfigField "String", "LOGIN_URL", appLoginUrl
 | 
			
		||||
            buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
 | 
			
		||||
            buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    flavorDimensions "build"
 | 
			
		||||
    productFlavors {
 | 
			
		||||
        githubConfig {
 | 
			
		||||
            versionNameSuffix '-github'
 | 
			
		||||
            dimension "build"
 | 
			
		||||
            buildConfigField "boolean", "GITHUB_VERSION", "true"
 | 
			
		||||
        }
 | 
			
		||||
        storeConfig {
 | 
			
		||||
            // As jenkins publishes to alpha first, this is the default suffix now.
 | 
			
		||||
            versionNameSuffix '-store'
 | 
			
		||||
            dimension "build"
 | 
			
		||||
            buildConfigField "boolean", "GITHUB_VERSION", "false"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    compile fileTree(dir: 'libs', include: ['*.jar'])
 | 
			
		||||
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
 | 
			
		||||
 | 
			
		||||
    // Testing
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta02'
 | 
			
		||||
    androidTestImplementation 'androidx.test:runner:1.1.0-beta02'
 | 
			
		||||
    // Espresso-contrib for DatePicker, RecyclerView, Drawer actions, Accessibility checks, CountingIdlingResource
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.0-beta02'
 | 
			
		||||
    // Espresso-intents for validation and stubbing of Intents
 | 
			
		||||
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0-beta02'
 | 
			
		||||
    implementation fileTree(include: ['*.jar'], dir: 'libs')
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
			
		||||
    // Android Support
 | 
			
		||||
    compile 'com.android.support:appcompat-v7:25.3.1'
 | 
			
		||||
    compile 'com.android.support:design:25.3.1'
 | 
			
		||||
    compile 'com.android.support:recyclerview-v7:25.3.1'
 | 
			
		||||
    compile 'com.android.support:support-v4:25.3.1'
 | 
			
		||||
    compile 'com.android.support:support-vector-drawable:25.3.1'
 | 
			
		||||
    compile 'com.android.support:customtabs:25.3.1'
 | 
			
		||||
    compile 'com.android.support:cardview-v7:25.3.1'
 | 
			
		||||
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
 | 
			
		||||
 | 
			
		||||
    // Firebase + crashlytics
 | 
			
		||||
    compile 'com.google.firebase:firebase-core:10.2.6'
 | 
			
		||||
    compile 'com.google.firebase:firebase-config:10.2.6'
 | 
			
		||||
    compile 'com.google.firebase:firebase-invites:10.2.6'
 | 
			
		||||
    compile('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
 | 
			
		||||
        transitive = true
 | 
			
		||||
    }
 | 
			
		||||
    implementation "androidx.appcompat:appcompat:$android_version"
 | 
			
		||||
    implementation "com.google.android.material:material:$android_version"
 | 
			
		||||
    implementation "androidx.recyclerview:recyclerview:$android_version"
 | 
			
		||||
    implementation "androidx.legacy:legacy-support-v4:$android_version"
 | 
			
		||||
    implementation "androidx.vectordrawable:vectordrawable:$android_version"
 | 
			
		||||
    implementation "androidx.browser:browser:$android_version"
 | 
			
		||||
    implementation "androidx.cardview:cardview:$android_version"
 | 
			
		||||
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
 | 
			
		||||
 | 
			
		||||
    //multidex
 | 
			
		||||
    compile 'com.android.support:multidex:1.0.1'
 | 
			
		||||
 | 
			
		||||
    // Intro
 | 
			
		||||
    compile 'agency.tango.android:material-intro-screen:0.0.5'
 | 
			
		||||
    implementation 'androidx.multidex:multidex:2.0.0'
 | 
			
		||||
 | 
			
		||||
    // About
 | 
			
		||||
    compile('com.mikepenz:aboutlibraries:5.9.6@aar') {
 | 
			
		||||
    implementation('com.mikepenz:aboutlibraries:6.2.0@aar') {
 | 
			
		||||
        transitive = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Retrofit + http logging + okhttp
 | 
			
		||||
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
 | 
			
		||||
    compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
 | 
			
		||||
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
 | 
			
		||||
    compile 'com.burgstaller:okhttp-digest:1.12'
 | 
			
		||||
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
 | 
			
		||||
    implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
 | 
			
		||||
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
 | 
			
		||||
    implementation 'com.burgstaller:okhttp-digest:1.12'
 | 
			
		||||
 | 
			
		||||
    // Material-ish things
 | 
			
		||||
    compile 'com.roughike:bottom-bar:2.3.1'
 | 
			
		||||
    compile 'com.melnykov:floatingactionbutton:1.3.0'
 | 
			
		||||
    compile 'com.github.jd-alexander:LikeButton:0.2.1'
 | 
			
		||||
    compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
 | 
			
		||||
    compile 'org.sufficientlysecure:html-textview:3.3'
 | 
			
		||||
    implementation 'com.ashokvarma.android:bottom-navigation-bar:2.0.5'
 | 
			
		||||
    implementation 'com.github.jd-alexander:LikeButton:0.2.3'
 | 
			
		||||
    implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
 | 
			
		||||
 | 
			
		||||
    // glide
 | 
			
		||||
    compile 'com.github.bumptech.glide:glide:3.7.0'
 | 
			
		||||
    implementation 'com.github.bumptech.glide:glide:4.1.1'
 | 
			
		||||
    implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
 | 
			
		||||
 | 
			
		||||
    // Asking politely users to rate the app
 | 
			
		||||
    compile 'com.github.stkent:amplify:1.5.0'
 | 
			
		||||
 | 
			
		||||
    // For the article reader
 | 
			
		||||
    compile 'com.klinkerapps:drag-dismiss-activity:1.4.0'
 | 
			
		||||
    implementation 'com.github.stkent:amplify:2.2.0'
 | 
			
		||||
 | 
			
		||||
    // Drawer
 | 
			
		||||
    compile('com.mikepenz:materialdrawer:5.9.2@aar') {
 | 
			
		||||
        transitive = true
 | 
			
		||||
    }
 | 
			
		||||
    compile 'com.anupcowkur:reservoir:3.1.0'
 | 
			
		||||
    implementation 'co.zsmb:materialdrawer-kt:2.0.1'
 | 
			
		||||
 | 
			
		||||
    // Themes
 | 
			
		||||
    implementation 'com.52inc:scoops:1.0.0'
 | 
			
		||||
    implementation 'com.jaredrummler:colorpicker:1.0.2'
 | 
			
		||||
    implementation 'com.github.rubensousa:floatingtoolbar:1.5.1'
 | 
			
		||||
 | 
			
		||||
    // Pager
 | 
			
		||||
    implementation 'me.relex:circleindicator:2.0.0@aar'
 | 
			
		||||
 | 
			
		||||
    implementation 'androidx.core:core-ktx:1.0.0'
 | 
			
		||||
 | 
			
		||||
    // Crash
 | 
			
		||||
    implementation 'ch.acra:acra-http:5.2.1'
 | 
			
		||||
    implementation 'ch.acra:acra-dialog:5.2.1'
 | 
			
		||||
 | 
			
		||||
    implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
 | 
			
		||||
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
 | 
			
		||||
 | 
			
		||||
    implementation "androidx.room:room-runtime:$room_version"
 | 
			
		||||
    kapt "androidx.room:room-compiler:$room_version"
 | 
			
		||||
 | 
			
		||||
    implementation "android.arch.work:work-runtime-ktx:$work_version"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply plugin: 'com.google.gms.google-services'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
afterEvaluate {
 | 
			
		||||
    initFabricPropertiesIfNeeded()
 | 
			
		||||
    initAppLoginPropertiesIfNeeded()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
def initFabricPropertiesIfNeeded() {
 | 
			
		||||
    def propertiesFile = file('fabric.properties')
 | 
			
		||||
def initAppLoginPropertiesIfNeeded() {
 | 
			
		||||
    def propertiesFile = file(System.getProperty("user.home") + '/.gradle/gradle.properties')
 | 
			
		||||
    if (!propertiesFile.exists()) {
 | 
			
		||||
        def commentMessage = "This is autogenerated fabric property from system environment to prevent key to be committed to source control."
 | 
			
		||||
        ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
 | 
			
		||||
            entry(key: "apiSecret", value: crashlyticsdemoApisecret)
 | 
			
		||||
            entry(key: "apiKey", value: crashlyticsdemoApikey)
 | 
			
		||||
        def commentMessage = "This is autogenerated local property from system environment to prevent key to be committed to source control."
 | 
			
		||||
        ant.propertyfile(file: System.getProperty("user.home") + "/.gradle/gradle.properties", comment: commentMessage) {
 | 
			
		||||
            entry(key: "appLoginUrl", value: System.getProperty("appLoginUrl"))
 | 
			
		||||
            entry(key: "appLoginUsername", value: System.getProperty("appLoginUsername"))
 | 
			
		||||
            entry(key: "appLoginPassword", value: System.getProperty("appLoginPassword"))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -30,25 +30,13 @@
 | 
			
		||||
    <fields>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##Retrofit
 | 
			
		||||
#-keep class com.google.gson.** { *; }
 | 
			
		||||
#-keep class com.google.inject.** { *; }
 | 
			
		||||
#-keep class org.apache.http.** { *; }
 | 
			
		||||
#-keep class org.apache.james.mime4j.** { *; }
 | 
			
		||||
#-keep class javax.inject.** { *; }
 | 
			
		||||
#-keep class retrofit.** { *; }
 | 
			
		||||
#-keepclassmembernames interface * {
 | 
			
		||||
#    @retrofit.http.* <methods>;
 | 
			
		||||
#}
 | 
			
		||||
#-keep class retrofit.** { *; }
 | 
			
		||||
#-keep class apps.amine.bou.readerforselfoss.api.selfoss.model.** { *; }
 | 
			
		||||
#-keepclassmembernames interface * {
 | 
			
		||||
#    @retrofit.http.* <methods>;
 | 
			
		||||
#}
 | 
			
		||||
-dontwarn okio.**
 | 
			
		||||
-dontwarn retrofit2.Platform$Java8
 | 
			
		||||
-keepattributes Signature
 | 
			
		||||
-keep class retrofit.** { *; }
 | 
			
		||||
-keepclasseswithmembers class * {
 | 
			
		||||
    @retrofit.http.* <methods>;
 | 
			
		||||
}
 | 
			
		||||
-keepattributes *Annotation*,Signature
 | 
			
		||||
-keepattributes Exceptions
 | 
			
		||||
-dontwarn okio.**
 | 
			
		||||
-dontwarn javax.annotation.Nullable
 | 
			
		||||
@@ -56,4 +44,22 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#Bottom bar lib
 | 
			
		||||
-dontwarn com.roughike.bottombar.**
 | 
			
		||||
-dontwarn com.roughike.bottombar.**
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# self signed glidemodule
 | 
			
		||||
-keep public class * implements com.bumptech.glide.module.GlideModule
 | 
			
		||||
-keep public class * extends com.bumptech.glide.AppGlideModule
 | 
			
		||||
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
 | 
			
		||||
  **[] $VALUES;
 | 
			
		||||
  public *;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
-dontwarn com.anupcowkur.reservoir.**
 | 
			
		||||
 | 
			
		||||
-dontwarn javax.annotation.**
 | 
			
		||||
 | 
			
		||||
-keep class android.support.v7.widget.SearchView { *; }
 | 
			
		||||
 | 
			
		||||
# maybe remove later ?
 | 
			
		||||
-keep class * extends androidx.fragment.app.Fragment
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,96 @@
 | 
			
		||||
{
 | 
			
		||||
  "formatVersion": 1,
 | 
			
		||||
  "database": {
 | 
			
		||||
    "version": 1,
 | 
			
		||||
    "identityHash": "08ca537d7ac9d4dd216e8e395d70801a",
 | 
			
		||||
    "entities": [
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "tags",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tag",
 | 
			
		||||
            "columnName": "tag",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "color",
 | 
			
		||||
            "columnName": "color",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "tag"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "sources",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "title",
 | 
			
		||||
            "columnName": "title",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tags",
 | 
			
		||||
            "columnName": "tags",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "spout",
 | 
			
		||||
            "columnName": "spout",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "error",
 | 
			
		||||
            "columnName": "error",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "icon",
 | 
			
		||||
            "columnName": "icon",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "views": [],
 | 
			
		||||
    "setupQueries": [
 | 
			
		||||
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
 | 
			
		||||
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"08ca537d7ac9d4dd216e8e395d70801a\")"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,176 @@
 | 
			
		||||
{
 | 
			
		||||
  "formatVersion": 1,
 | 
			
		||||
  "database": {
 | 
			
		||||
    "version": 2,
 | 
			
		||||
    "identityHash": "6fa6944b04100d68eab61039876a8804",
 | 
			
		||||
    "entities": [
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "tags",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tag",
 | 
			
		||||
            "columnName": "tag",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "color",
 | 
			
		||||
            "columnName": "color",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "tag"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "sources",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "title",
 | 
			
		||||
            "columnName": "title",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tags",
 | 
			
		||||
            "columnName": "tags",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "spout",
 | 
			
		||||
            "columnName": "spout",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "error",
 | 
			
		||||
            "columnName": "error",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "icon",
 | 
			
		||||
            "columnName": "icon",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "items",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "datetime",
 | 
			
		||||
            "columnName": "datetime",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "title",
 | 
			
		||||
            "columnName": "title",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "content",
 | 
			
		||||
            "columnName": "content",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "starred",
 | 
			
		||||
            "columnName": "starred",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "thumbnail",
 | 
			
		||||
            "columnName": "thumbnail",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "icon",
 | 
			
		||||
            "columnName": "icon",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "link",
 | 
			
		||||
            "columnName": "link",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "sourcetitle",
 | 
			
		||||
            "columnName": "sourcetitle",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tags",
 | 
			
		||||
            "columnName": "tags",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "views": [],
 | 
			
		||||
    "setupQueries": [
 | 
			
		||||
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
 | 
			
		||||
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6fa6944b04100d68eab61039876a8804\")"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,226 @@
 | 
			
		||||
{
 | 
			
		||||
  "formatVersion": 1,
 | 
			
		||||
  "database": {
 | 
			
		||||
    "version": 3,
 | 
			
		||||
    "identityHash": "7ad9c4961992c13b670128485ebb3efc",
 | 
			
		||||
    "entities": [
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "tags",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `color` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`tag`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tag",
 | 
			
		||||
            "columnName": "tag",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "color",
 | 
			
		||||
            "columnName": "color",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "tag"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "sources",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `spout` TEXT NOT NULL, `error` TEXT NOT NULL, `icon` TEXT NOT NULL, PRIMARY KEY(`id`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "title",
 | 
			
		||||
            "columnName": "title",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tags",
 | 
			
		||||
            "columnName": "tags",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "spout",
 | 
			
		||||
            "columnName": "spout",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "error",
 | 
			
		||||
            "columnName": "error",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "icon",
 | 
			
		||||
            "columnName": "icon",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "items",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "datetime",
 | 
			
		||||
            "columnName": "datetime",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "title",
 | 
			
		||||
            "columnName": "title",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "content",
 | 
			
		||||
            "columnName": "content",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "starred",
 | 
			
		||||
            "columnName": "starred",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "thumbnail",
 | 
			
		||||
            "columnName": "thumbnail",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "icon",
 | 
			
		||||
            "columnName": "icon",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "link",
 | 
			
		||||
            "columnName": "link",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "sourcetitle",
 | 
			
		||||
            "columnName": "sourcetitle",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "tags",
 | 
			
		||||
            "columnName": "tags",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": false
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "tableName": "actions",
 | 
			
		||||
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL)",
 | 
			
		||||
        "fields": [
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "id",
 | 
			
		||||
            "columnName": "id",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "articleId",
 | 
			
		||||
            "columnName": "articleid",
 | 
			
		||||
            "affinity": "TEXT",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "read",
 | 
			
		||||
            "columnName": "read",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unread",
 | 
			
		||||
            "columnName": "unread",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "starred",
 | 
			
		||||
            "columnName": "starred",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "fieldPath": "unstarred",
 | 
			
		||||
            "columnName": "unstarred",
 | 
			
		||||
            "affinity": "INTEGER",
 | 
			
		||||
            "notNull": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "primaryKey": {
 | 
			
		||||
          "columnNames": [
 | 
			
		||||
            "id"
 | 
			
		||||
          ],
 | 
			
		||||
          "autoGenerate": true
 | 
			
		||||
        },
 | 
			
		||||
        "indices": [],
 | 
			
		||||
        "foreignKeys": []
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "views": [],
 | 
			
		||||
    "setupQueries": [
 | 
			
		||||
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
 | 
			
		||||
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7ad9c4961992c13b670128485ebb3efc\")"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
// TODO: test source adding
 | 
			
		||||
@@ -0,0 +1,100 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import androidx.test.InstrumentationRegistry
 | 
			
		||||
import androidx.test.espresso.Espresso.onView
 | 
			
		||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.click
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.pressBack
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.pressKey
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.typeText
 | 
			
		||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
			
		||||
import androidx.test.espresso.contrib.DrawerActions
 | 
			
		||||
import androidx.test.espresso.intent.Intents
 | 
			
		||||
import androidx.test.espresso.intent.Intents.intended
 | 
			
		||||
import androidx.test.espresso.intent.Intents.times
 | 
			
		||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withId
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
			
		||||
import androidx.test.rule.ActivityTestRule
 | 
			
		||||
import androidx.test.runner.AndroidJUnit4
 | 
			
		||||
import android.view.KeyEvent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import org.junit.After
 | 
			
		||||
import org.junit.Before
 | 
			
		||||
import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
class HomeActivityEspressoTest {
 | 
			
		||||
    lateinit var context: Context
 | 
			
		||||
 | 
			
		||||
    @Rule @JvmField
 | 
			
		||||
    val rule = ActivityTestRule(HomeActivity::class.java, true, false)
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    fun clearData() {
 | 
			
		||||
        context = InstrumentationRegistry.getInstrumentation().targetContext
 | 
			
		||||
 | 
			
		||||
        val editor =
 | 
			
		||||
                context
 | 
			
		||||
                        .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
                        .edit()
 | 
			
		||||
        editor.clear()
 | 
			
		||||
 | 
			
		||||
        editor.putString("url", BuildConfig.LOGIN_URL)
 | 
			
		||||
        editor.putString("login", BuildConfig.LOGIN_USERNAME)
 | 
			
		||||
        editor.putString("password", BuildConfig.LOGIN_PASSWORD)
 | 
			
		||||
 | 
			
		||||
        editor.commit()
 | 
			
		||||
 | 
			
		||||
        Intents.init()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun menuItems() {
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        onView(
 | 
			
		||||
                withMenu(
 | 
			
		||||
                        id = R.id.action_search,
 | 
			
		||||
                        titleId = R.string.menu_home_search
 | 
			
		||||
                )
 | 
			
		||||
        ).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.search_bar)).check(matches(isDisplayed()))
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.search_src_text)).perform(
 | 
			
		||||
                typeText("android"),
 | 
			
		||||
                pressKey(KeyEvent.KEYCODE_SEARCH),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
 | 
			
		||||
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(context)
 | 
			
		||||
 | 
			
		||||
        onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
 | 
			
		||||
                .perform(click())
 | 
			
		||||
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(context)
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.action_disconnect)).perform(click())
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(LoginActivity::class.java.name), times(1))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: test articles opening and actions for cards and lists
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
    fun releaseIntents() {
 | 
			
		||||
        Intents.release()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,177 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import androidx.test.InstrumentationRegistry
 | 
			
		||||
import androidx.test.espresso.Espresso.onView
 | 
			
		||||
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.click
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.pressBack
 | 
			
		||||
import androidx.test.espresso.action.ViewActions.typeText
 | 
			
		||||
import androidx.test.espresso.assertion.ViewAssertions.matches
 | 
			
		||||
import androidx.test.espresso.intent.Intents
 | 
			
		||||
import androidx.test.espresso.intent.Intents.intended
 | 
			
		||||
import androidx.test.espresso.intent.Intents.times
 | 
			
		||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.isRoot
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withId
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers.withText
 | 
			
		||||
import androidx.test.rule.ActivityTestRule
 | 
			
		||||
import androidx.test.runner.AndroidJUnit4
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import com.mikepenz.aboutlibraries.ui.LibsActivity
 | 
			
		||||
import org.junit.After
 | 
			
		||||
import org.junit.Before
 | 
			
		||||
import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
class LoginActivityEspressoTest {
 | 
			
		||||
 | 
			
		||||
    @Rule @JvmField
 | 
			
		||||
    val rule = ActivityTestRule(LoginActivity::class.java, true, false)
 | 
			
		||||
 | 
			
		||||
    private lateinit var context: Context
 | 
			
		||||
    private lateinit var url: String
 | 
			
		||||
    private lateinit var username: String
 | 
			
		||||
    private lateinit var password: String
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    fun setUp() {
 | 
			
		||||
        context = InstrumentationRegistry.getInstrumentation().targetContext
 | 
			
		||||
        val editor =
 | 
			
		||||
                context
 | 
			
		||||
                        .getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
                        .edit()
 | 
			
		||||
        editor.clear()
 | 
			
		||||
        editor.commit()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        url = BuildConfig.LOGIN_URL
 | 
			
		||||
        username = BuildConfig.LOGIN_USERNAME
 | 
			
		||||
        password = BuildConfig.LOGIN_PASSWORD
 | 
			
		||||
 | 
			
		||||
        Intents.init()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun menuItems() {
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        openActionBarOverflowOrOptionsMenu(context)
 | 
			
		||||
 | 
			
		||||
        onView(withText(R.string.action_about)).perform(click())
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(LibsActivity::class.java.name), times(1))
 | 
			
		||||
 | 
			
		||||
        onView(isRoot()).perform(pressBack())
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(LoginActivity::class.java.name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun wrongLoginUrl() {
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.loginProgress))
 | 
			
		||||
                .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO: Add tests for multiple false urls with dialog
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun emptyAuthData() {
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.withLogin)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
        onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.loginView)).perform(click()).perform(
 | 
			
		||||
                typeText(username),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.passwordLayout)).check(
 | 
			
		||||
                matches(
 | 
			
		||||
                        isHintOrErrorEnabled()
 | 
			
		||||
                )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun wrongAuthData() {
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.withLogin)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.loginView)).perform(click()).perform(
 | 
			
		||||
                typeText(username),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.passwordView)).perform(click()).perform(
 | 
			
		||||
                typeText("WRONGPASS"),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
        onView(withId(R.id.loginLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
        onView(withId(R.id.passwordLayout)).check(matches(isHintOrErrorEnabled()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun workingAuth() {
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(Intent())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.withLogin)).perform(click())
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.loginView)).perform(click()).perform(
 | 
			
		||||
                typeText(username),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.passwordView)).perform(click()).perform(
 | 
			
		||||
                typeText(password),
 | 
			
		||||
                closeSoftKeyboard()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        onView(withId(R.id.signInButton)).perform(click())
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(HomeActivity::class.java.name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
    fun releaseIntents() {
 | 
			
		||||
        Intents.release()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.test.InstrumentationRegistry.getInstrumentation
 | 
			
		||||
import androidx.test.espresso.intent.Intents
 | 
			
		||||
import androidx.test.espresso.intent.Intents.intended
 | 
			
		||||
import androidx.test.espresso.intent.Intents.times
 | 
			
		||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
 | 
			
		||||
import androidx.test.rule.ActivityTestRule
 | 
			
		||||
import androidx.test.runner.AndroidJUnit4
 | 
			
		||||
import org.junit.After
 | 
			
		||||
 | 
			
		||||
import org.junit.Before
 | 
			
		||||
import org.junit.Rule
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.runner.RunWith
 | 
			
		||||
 | 
			
		||||
@RunWith(AndroidJUnit4::class)
 | 
			
		||||
class MainActivityEspressoTest {
 | 
			
		||||
 | 
			
		||||
    lateinit var intent: Intent
 | 
			
		||||
    lateinit var preferencesEditor: SharedPreferences.Editor
 | 
			
		||||
 | 
			
		||||
    @Rule @JvmField
 | 
			
		||||
    val rule = ActivityTestRule(MainActivity::class.java, true, false)
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    fun setUp() {
 | 
			
		||||
        intent = Intent()
 | 
			
		||||
        val context = getInstrumentation().targetContext
 | 
			
		||||
 | 
			
		||||
        // create a SharedPreferences editor
 | 
			
		||||
        preferencesEditor = PreferenceManager.getDefaultSharedPreferences(context).edit()
 | 
			
		||||
 | 
			
		||||
        Intents.init()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun checkFirstOpenLaunchesIntro() {
 | 
			
		||||
        preferencesEditor.putBoolean("firstStart", true)
 | 
			
		||||
        preferencesEditor.commit()
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(intent)
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(MainActivity::class.java.name))
 | 
			
		||||
        intended(hasComponent(LoginActivity::class.java.name), times(0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    fun checkNotFirstOpenLaunchesLogin() {
 | 
			
		||||
        preferencesEditor.putBoolean("firstStart", false)
 | 
			
		||||
        preferencesEditor.commit()
 | 
			
		||||
 | 
			
		||||
        rule.launchActivity(intent)
 | 
			
		||||
 | 
			
		||||
        intended(hasComponent(MainActivity::class.java.name))
 | 
			
		||||
        intended(hasComponent(LoginActivity::class.java.name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After
 | 
			
		||||
    fun releaseIntents() {
 | 
			
		||||
        Intents.release()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import com.google.android.material.textfield.TextInputLayout
 | 
			
		||||
import androidx.test.espresso.matcher.ViewMatchers
 | 
			
		||||
import android.view.View
 | 
			
		||||
import org.hamcrest.Description
 | 
			
		||||
import org.hamcrest.Matcher
 | 
			
		||||
import org.hamcrest.Matchers
 | 
			
		||||
import org.hamcrest.TypeSafeMatcher
 | 
			
		||||
 | 
			
		||||
fun isHintOrErrorEnabled(): Matcher<View> =
 | 
			
		||||
        object : TypeSafeMatcher<View>() {
 | 
			
		||||
            override fun describeTo(description: Description?) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun matchesSafely(item: View?): Boolean {
 | 
			
		||||
                if (item !is TextInputLayout) {
 | 
			
		||||
                    return false
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return item.isHintEnabled || item.isErrorEnabled
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
fun withMenu(id: Int, titleId: Int): Matcher<View> =
 | 
			
		||||
        Matchers.anyOf(
 | 
			
		||||
                ViewMatchers.withId(id),
 | 
			
		||||
                ViewMatchers.withText(titleId)
 | 
			
		||||
        )
 | 
			
		||||
@@ -1,12 +1,10 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    package="apps.amine.bou.readerforselfoss">
 | 
			
		||||
    package="apps.amine.bou.readerforselfoss"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools">
 | 
			
		||||
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
 | 
			
		||||
    <!-- For firebase only -->
 | 
			
		||||
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.WAKE_LOCK" />
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
 | 
			
		||||
    <application
 | 
			
		||||
        android:name=".MyApp"
 | 
			
		||||
@@ -14,7 +12,8 @@
 | 
			
		||||
        android:icon="@mipmap/ic_launcher"
 | 
			
		||||
        android:label="@string/app_name"
 | 
			
		||||
        android:supportsRtl="true"
 | 
			
		||||
        android:theme="@style/AppTheme">
 | 
			
		||||
        android:networkSecurityConfig="@xml/network_security_config"
 | 
			
		||||
        android:theme="@style/NoBar">
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:theme="@style/SplashTheme">
 | 
			
		||||
@@ -23,16 +22,15 @@
 | 
			
		||||
 | 
			
		||||
                <category android:name="android.intent.category.LAUNCHER" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
 | 
			
		||||
            <meta-data android:name="android.app.shortcuts"
 | 
			
		||||
                android:resource="@xml/shortcuts" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".IntroActivity"
 | 
			
		||||
            android:theme="@style/Theme.Intro">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".LoginActivity"
 | 
			
		||||
            android:name=".LoginActivity"
 | 
			
		||||
            android:label="@string/title_activity_login">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".HomeActivity"
 | 
			
		||||
            android:theme="@style/NoBar">
 | 
			
		||||
        <activity android:name=".HomeActivity">
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".settings.SettingsActivity"
 | 
			
		||||
@@ -42,13 +40,15 @@
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value="apps.amine.bou.readerforselfoss.HomeActivity" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".SourcesActivity"
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".SourcesActivity"
 | 
			
		||||
            android:parentActivityName=".HomeActivity">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
                android:value=".HomeActivity" />
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".AddSourceActivity"
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".AddSourceActivity"
 | 
			
		||||
            android:parentActivityName=".SourcesActivity">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.PARENT_ACTIVITY"
 | 
			
		||||
@@ -62,9 +62,21 @@
 | 
			
		||||
                <data android:mimeType="text/plain" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
        <activity android:name=".ReaderActivity"
 | 
			
		||||
            android:theme="@style/DragDismissTheme">
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".ReaderActivity">
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="apps.amine.bou.readerforselfoss.utils.glide.SelfSignedGlideModule"
 | 
			
		||||
            android:value="GlideModule" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.webkit.WebView.MetricsOptOut"
 | 
			
		||||
            android:value="true" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
 | 
			
		||||
            android:value="true" />
 | 
			
		||||
 | 
			
		||||
        <meta-data android:name="android.max_aspect" android:value="2.1" />
 | 
			
		||||
    </application>
 | 
			
		||||
 | 
			
		||||
</manifest>
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/ic_launcher-web.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
@@ -1,60 +1,134 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.constraint.ConstraintLayout
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.*
 | 
			
		||||
import android.widget.AdapterView
 | 
			
		||||
import android.widget.ArrayAdapter
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import android.widget.ProgressBar
 | 
			
		||||
import android.widget.Spinner
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Spout
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_add_source.*
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import android.graphics.PorterDuff
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddSourceActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private var mSpoutsValue: String? = null
 | 
			
		||||
    private lateinit var api: SelfossApi
 | 
			
		||||
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(this@AddSourceActivity)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.activity_add_source)
 | 
			
		||||
 | 
			
		||||
        val mProgress = findViewById(R.id.progress) as ProgressBar
 | 
			
		||||
        val mForm = findViewById(R.id.formContainer) as ConstraintLayout
 | 
			
		||||
        val mNameInput = findViewById(R.id.nameInput) as EditText
 | 
			
		||||
        val mSourceUri = findViewById(R.id.sourceUri) as EditText
 | 
			
		||||
        val mTags = findViewById(R.id.tags) as EditText
 | 
			
		||||
        val mSpoutsSpinner = findViewById(R.id.spoutsSpinner) as Spinner
 | 
			
		||||
        val mSaveBtn = findViewById(R.id.saveBtn) as Button
 | 
			
		||||
        var api: SelfossApi? = null
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, toolbar)
 | 
			
		||||
        if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val drawable = nameInput.background
 | 
			
		||||
        drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        // TODO: clean
 | 
			
		||||
        if(Build.VERSION.SDK_INT > 16) {
 | 
			
		||||
            nameInput.background = drawable
 | 
			
		||||
        } else{
 | 
			
		||||
            nameInput.setBackgroundDrawable(drawable)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val drawable1 = sourceUri.background
 | 
			
		||||
        drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
        if(Build.VERSION.SDK_INT > 16) {
 | 
			
		||||
            sourceUri.background = drawable1
 | 
			
		||||
        } else{
 | 
			
		||||
            sourceUri.setBackgroundDrawable(drawable1)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val drawable2 = tags.background
 | 
			
		||||
        drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
 | 
			
		||||
 | 
			
		||||
        if(Build.VERSION.SDK_INT > 16) {
 | 
			
		||||
            tags.background = drawable2
 | 
			
		||||
        } else{
 | 
			
		||||
            tags.setBackgroundDrawable(drawable2)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(toolbar)
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            api = SelfossApi(this)
 | 
			
		||||
            val prefs = PreferenceManager.getDefaultSharedPreferences(this)
 | 
			
		||||
            api = SelfossApi(
 | 
			
		||||
                this,
 | 
			
		||||
                this@AddSourceActivity,
 | 
			
		||||
                prefs.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
                prefs.getString("api_timeout", "-1").toLong(),
 | 
			
		||||
                prefs.getBoolean("should_log_everything", false)
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: IllegalArgumentException) {
 | 
			
		||||
            mustLoginToAddSource()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput)
 | 
			
		||||
 | 
			
		||||
        saveBtn.setTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
        val intent = intent
 | 
			
		||||
        if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
 | 
			
		||||
            mSourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
 | 
			
		||||
            mNameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
 | 
			
		||||
        saveBtn.setOnClickListener {
 | 
			
		||||
            handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        mSaveBtn.setOnClickListener { handleSaveSource(mTags, mNameInput.text.toString(), mSourceUri.text.toString(), api!!) }
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        val config = Config(this)
 | 
			
		||||
 | 
			
		||||
        if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(false, this@AddSourceActivity)) {
 | 
			
		||||
            mustLoginToAddSource()
 | 
			
		||||
        } else {
 | 
			
		||||
            handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleSpoutsSpinner(
 | 
			
		||||
        spoutsSpinner: Spinner,
 | 
			
		||||
        api: SelfossApi?,
 | 
			
		||||
        mProgress: ProgressBar,
 | 
			
		||||
        formContainer: ConstraintLayout
 | 
			
		||||
    ) {
 | 
			
		||||
        val spoutsKV = HashMap<String, String>()
 | 
			
		||||
        mSpoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
 | 
			
		||||
            override fun onItemSelected(adapterView: AdapterView<*>, view: View, i: Int, l: Long) {
 | 
			
		||||
                val spoutName = (view as TextView).text.toString()
 | 
			
		||||
                mSpoutsValue = spoutsKV[spoutName]
 | 
			
		||||
        spoutsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
 | 
			
		||||
            override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) {
 | 
			
		||||
                if (view != null) {
 | 
			
		||||
                    val spoutName = (view as TextView).text.toString()
 | 
			
		||||
                    mSpoutsValue = spoutsKV[spoutName]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onNothingSelected(adapterView: AdapterView<*>) {
 | 
			
		||||
@@ -62,44 +136,59 @@ class AddSourceActivity : AppCompatActivity() {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val config = Config(this)
 | 
			
		||||
        var items: Map<String, Spout>
 | 
			
		||||
        api!!.spouts().enqueue(object : Callback<Map<String, Spout>> {
 | 
			
		||||
            override fun onResponse(
 | 
			
		||||
                call: Call<Map<String, Spout>>,
 | 
			
		||||
                response: Response<Map<String, Spout>>
 | 
			
		||||
            ) {
 | 
			
		||||
                if (response.body() != null) {
 | 
			
		||||
                    items = response.body()!!
 | 
			
		||||
 | 
			
		||||
        if (config.baseUrl.isEmpty() || !isUrlValid(config.baseUrl)) {
 | 
			
		||||
            mustLoginToAddSource()
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
            var items: Map<String, Spout>
 | 
			
		||||
            api!!.spouts().enqueue(object : Callback<Map<String, Spout>> {
 | 
			
		||||
                override fun onResponse(call: Call<Map<String, Spout>>, response: Response<Map<String, Spout>>) {
 | 
			
		||||
                    if (response.body() != null) {
 | 
			
		||||
                        items = response.body()!!
 | 
			
		||||
 | 
			
		||||
                        val itemsStrings = items.map { it.value.name }
 | 
			
		||||
                        for ((key, value) in items) {
 | 
			
		||||
                            spoutsKV.put(value.name, key)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        mProgress.visibility = View.GONE
 | 
			
		||||
                        mForm.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
                        val spinnerArrayAdapter = ArrayAdapter(this@AddSourceActivity, android.R.layout.simple_spinner_item, itemsStrings)
 | 
			
		||||
                        spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
 | 
			
		||||
                        mSpoutsSpinner.adapter = spinnerArrayAdapter
 | 
			
		||||
 | 
			
		||||
                    } else {
 | 
			
		||||
                        handleProblemWithSpouts()
 | 
			
		||||
                    val itemsStrings = items.map { it.value.name }
 | 
			
		||||
                    for ((key, value) in items) {
 | 
			
		||||
                        spoutsKV[value.name] = key
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
 | 
			
		||||
                    mProgress.visibility = View.GONE
 | 
			
		||||
                    formContainer.visibility = View.VISIBLE
 | 
			
		||||
 | 
			
		||||
                    val spinnerArrayAdapter =
 | 
			
		||||
                        ArrayAdapter(
 | 
			
		||||
                            this@AddSourceActivity,
 | 
			
		||||
                            android.R.layout.simple_spinner_item,
 | 
			
		||||
                            itemsStrings
 | 
			
		||||
                        )
 | 
			
		||||
                    spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
 | 
			
		||||
                    spoutsSpinner.adapter = spinnerArrayAdapter
 | 
			
		||||
                } else {
 | 
			
		||||
                    handleProblemWithSpouts()
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                private fun handleProblemWithSpouts() {
 | 
			
		||||
                    Toast.makeText(this@AddSourceActivity, R.string.cant_get_spouts, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                    mProgress.visibility = View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
 | 
			
		||||
                handleProblemWithSpouts()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private fun handleProblemWithSpouts() {
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    this@AddSourceActivity,
 | 
			
		||||
                    R.string.cant_get_spouts,
 | 
			
		||||
                    Toast.LENGTH_SHORT
 | 
			
		||||
                ).show()
 | 
			
		||||
                mProgress.visibility = View.GONE
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun maybeGetDetailsFromIntentSharing(
 | 
			
		||||
        intent: Intent,
 | 
			
		||||
        sourceUri: EditText,
 | 
			
		||||
        nameInput: EditText
 | 
			
		||||
    ) {
 | 
			
		||||
        if (Intent.ACTION_SEND == intent.action && "text/plain" == intent.type) {
 | 
			
		||||
            sourceUri.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
 | 
			
		||||
            nameInput.setText(intent.getStringExtra(Intent.EXTRA_TITLE))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -110,22 +199,42 @@ class AddSourceActivity : AppCompatActivity() {
 | 
			
		||||
        finish()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleSaveSource(mTags: EditText, title: String, url: String, api: SelfossApi) {
 | 
			
		||||
    private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
 | 
			
		||||
 | 
			
		||||
        if (title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()) {
 | 
			
		||||
        val sourceDetailsAvailable =
 | 
			
		||||
            title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
 | 
			
		||||
 | 
			
		||||
        if (sourceDetailsAvailable) {
 | 
			
		||||
            Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
 | 
			
		||||
        } else {
 | 
			
		||||
            api.createSource(title, url, mSpoutsValue!!, mTags.text.toString(), "").enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
 | 
			
		||||
            api.createSource(
 | 
			
		||||
                title,
 | 
			
		||||
                url,
 | 
			
		||||
                mSpoutsValue!!,
 | 
			
		||||
                tags.text.toString(),
 | 
			
		||||
                ""
 | 
			
		||||
            ).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                        finish()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@AddSourceActivity,
 | 
			
		||||
                            R.string.cant_create_source,
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                    Toast.makeText(this@AddSourceActivity, R.string.cant_create_source, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        this@AddSourceActivity,
 | 
			
		||||
                        R.string.cant_create_source,
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,57 +0,0 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import agency.tango.materialintroscreen.MaterialIntroActivity
 | 
			
		||||
import agency.tango.materialintroscreen.MessageButtonBehaviour
 | 
			
		||||
import agency.tango.materialintroscreen.SlideFragmentBuilder
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IntroActivity : MaterialIntroActivity() {
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        addSlide(SlideFragmentBuilder()
 | 
			
		||||
                .backgroundColor(R.color.colorPrimary)
 | 
			
		||||
                .buttonsColor(R.color.colorAccent)
 | 
			
		||||
                .image(R.mipmap.ic_launcher)
 | 
			
		||||
                .title(getString(R.string.intro_hello_title))
 | 
			
		||||
                .description(getString(R.string.intro_hello_message))
 | 
			
		||||
                .build())
 | 
			
		||||
 | 
			
		||||
        addSlide(SlideFragmentBuilder()
 | 
			
		||||
                .backgroundColor(R.color.colorAccent)
 | 
			
		||||
                .buttonsColor(R.color.colorPrimary)
 | 
			
		||||
                .image(R.drawable.ic_info_outline_white_48dp)
 | 
			
		||||
                .title(getString(R.string.intro_needs_selfoss_title))
 | 
			
		||||
                .description(getString(R.string.intro_needs_selfoss_message))
 | 
			
		||||
                .build(),
 | 
			
		||||
                MessageButtonBehaviour(View.OnClickListener {
 | 
			
		||||
                    val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://selfoss.aditu.de"))
 | 
			
		||||
                    startActivity(browserIntent)
 | 
			
		||||
                }, getString(R.string.intro_needs_selfoss_link)))
 | 
			
		||||
 | 
			
		||||
        addSlide(SlideFragmentBuilder()
 | 
			
		||||
                .backgroundColor(R.color.colorPrimaryDark)
 | 
			
		||||
                .buttonsColor(R.color.colorAccentDark)
 | 
			
		||||
                .image(R.drawable.ic_thumb_up_white_48dp)
 | 
			
		||||
                .title(getString(R.string.intro_all_set_title))
 | 
			
		||||
                .description(getString(R.string.intro_all_set_message))
 | 
			
		||||
                .build())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onFinish() {
 | 
			
		||||
        super.onFinish()
 | 
			
		||||
        val getPrefs = PreferenceManager.getDefaultSharedPreferences(baseContext)
 | 
			
		||||
        val e = getPrefs.edit()
 | 
			
		||||
        e.putBoolean("firstStart", false)
 | 
			
		||||
        e.apply()
 | 
			
		||||
        val intent = Intent(this, LoginActivity::class.java)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        finish()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,114 +6,116 @@ import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.design.widget.TextInputLayout
 | 
			
		||||
import android.support.v7.app.AlertDialog
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.inputmethod.EditorInfo
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.EditText
 | 
			
		||||
import android.widget.Switch
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.checkAndDisplayStoreApk
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isUrlValid
 | 
			
		||||
import com.google.firebase.analytics.FirebaseAnalytics
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import com.mikepenz.aboutlibraries.Libs
 | 
			
		||||
import com.mikepenz.aboutlibraries.LibsBuilder
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_login.*
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private var settings: SharedPreferences? = null
 | 
			
		||||
    private var mProgressView: View? = null
 | 
			
		||||
    private var mUrlView: EditText? = null
 | 
			
		||||
    private var mLoginView: TextView? = null
 | 
			
		||||
    private var mHTTPLoginView: TextView? = null
 | 
			
		||||
    private var mPasswordView: EditText? = null
 | 
			
		||||
    private var mHTTPPasswordView: EditText? = null
 | 
			
		||||
    private var inValidCount: Int = 0
 | 
			
		||||
    private var isWithSelfSignedCert = false
 | 
			
		||||
    private var isWithLogin = false
 | 
			
		||||
    private var isWithHTTPLogin = false
 | 
			
		||||
    private var mLoginFormView: View? = null
 | 
			
		||||
    private var  mFirebaseAnalytics: FirebaseAnalytics? = null
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private lateinit var settings: SharedPreferences
 | 
			
		||||
    private lateinit var editor: SharedPreferences.Editor
 | 
			
		||||
    private lateinit var userIdentifier: String
 | 
			
		||||
    private var logErrors: Boolean = false
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(this@LoginActivity)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.activity_login)
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(toolbar)
 | 
			
		||||
 | 
			
		||||
        handleBaseUrlFail()
 | 
			
		||||
 | 
			
		||||
        settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
        if (settings!!.getString("url", "").isNotEmpty()) {
 | 
			
		||||
        userIdentifier = settings.getString("unique_id", "")
 | 
			
		||||
        logErrors = settings.getBoolean("login_debug", false)
 | 
			
		||||
 | 
			
		||||
        editor = settings.edit()
 | 
			
		||||
 | 
			
		||||
        if (settings.getString("url", "").isNotEmpty()) {
 | 
			
		||||
            goToMain()
 | 
			
		||||
        } else {
 | 
			
		||||
            checkAndDisplayStoreApk(this@LoginActivity)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        isWithLogin = false
 | 
			
		||||
        isWithHTTPLogin = false
 | 
			
		||||
        inValidCount = 0
 | 
			
		||||
        handleActions()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        mFirebaseAnalytics = FirebaseAnalytics.getInstance(this)
 | 
			
		||||
        mUrlView = findViewById(R.id.url) as EditText
 | 
			
		||||
        mLoginView = findViewById(R.id.login) as TextView
 | 
			
		||||
        mHTTPLoginView = findViewById(R.id.httpLogin) as TextView
 | 
			
		||||
        mPasswordView = findViewById(R.id.password) as EditText
 | 
			
		||||
        mHTTPPasswordView = findViewById(R.id.httpPassword) as EditText
 | 
			
		||||
        mLoginFormView = findViewById(R.id.login_form)
 | 
			
		||||
        mProgressView = findViewById(R.id.login_progress)
 | 
			
		||||
    private fun handleActions() {
 | 
			
		||||
 | 
			
		||||
        val mSwitch = findViewById(R.id.withLogin) as Switch
 | 
			
		||||
        val mHTTPSwitch = findViewById(R.id.withHttpLogin) as Switch
 | 
			
		||||
        val mLoginLayout = findViewById(R.id.loginLayout) as TextInputLayout
 | 
			
		||||
        val mHTTPLoginLayout = findViewById(R.id.httpLoginInput) as TextInputLayout
 | 
			
		||||
        val mPasswordLayout = findViewById(R.id.passwordLayout) as TextInputLayout
 | 
			
		||||
        val mHTTPPasswordLayout = findViewById(R.id.httpPasswordInput) as TextInputLayout
 | 
			
		||||
        val mEmailSignInButton = findViewById(R.id.email_sign_in_button) as Button
 | 
			
		||||
        withSelfhostedCert.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
            isWithSelfSignedCert = !isWithSelfSignedCert
 | 
			
		||||
            val visi: Int = if (b) View.VISIBLE else View.GONE
 | 
			
		||||
 | 
			
		||||
        mPasswordView!!.setOnEditorActionListener(TextView.OnEditorActionListener { _, id, _ ->
 | 
			
		||||
            if (id == R.id.login || id == EditorInfo.IME_NULL) {
 | 
			
		||||
                attemptLogin()
 | 
			
		||||
                return@OnEditorActionListener true
 | 
			
		||||
            warningText.visibility = visi
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        passwordView.setOnEditorActionListener(
 | 
			
		||||
            TextView.OnEditorActionListener { _, id, _ ->
 | 
			
		||||
                if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
 | 
			
		||||
                    attemptLogin()
 | 
			
		||||
                    return@OnEditorActionListener true
 | 
			
		||||
                }
 | 
			
		||||
                false
 | 
			
		||||
            }
 | 
			
		||||
            false
 | 
			
		||||
        })
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mEmailSignInButton.setOnClickListener { attemptLogin() }
 | 
			
		||||
        signInButton.setOnClickListener { attemptLogin() }
 | 
			
		||||
 | 
			
		||||
        mSwitch.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
        withLogin.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
            isWithLogin = !isWithLogin
 | 
			
		||||
            val visi: Int
 | 
			
		||||
            if (b) {
 | 
			
		||||
                visi = View.VISIBLE
 | 
			
		||||
            val visi: Int = if (b) View.VISIBLE else View.GONE
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                visi = View.GONE
 | 
			
		||||
            }
 | 
			
		||||
            mLoginLayout.visibility = visi
 | 
			
		||||
            mPasswordLayout.visibility = visi
 | 
			
		||||
            loginLayout.visibility = visi
 | 
			
		||||
            passwordLayout.visibility = visi
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mHTTPSwitch.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
        withHttpLogin.setOnCheckedChangeListener { _, b ->
 | 
			
		||||
            isWithHTTPLogin = !isWithHTTPLogin
 | 
			
		||||
            val visi: Int
 | 
			
		||||
            if (b) {
 | 
			
		||||
                visi = View.VISIBLE
 | 
			
		||||
            val visi: Int = if (b) View.VISIBLE else View.GONE
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                visi = View.GONE
 | 
			
		||||
            }
 | 
			
		||||
            mHTTPLoginLayout.visibility = visi
 | 
			
		||||
            mHTTPPasswordLayout.visibility = visi
 | 
			
		||||
            httpLoginInput.visibility = visi
 | 
			
		||||
            httpPasswordInput.visibility = visi
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleBaseUrlFail() {
 | 
			
		||||
        if (intent.getBooleanExtra("baseUrlFail", false)) {
 | 
			
		||||
            val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
            alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
			
		||||
            alertDialog.setMessage(getString(R.string.base_url_error))
 | 
			
		||||
            alertDialog.setButton(
 | 
			
		||||
                AlertDialog.BUTTON_NEUTRAL,
 | 
			
		||||
                "OK",
 | 
			
		||||
                { dialog, _ -> dialog.dismiss() }
 | 
			
		||||
            )
 | 
			
		||||
            alertDialog.show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -126,124 +128,169 @@ class LoginActivity : AppCompatActivity() {
 | 
			
		||||
    private fun attemptLogin() {
 | 
			
		||||
 | 
			
		||||
        // Reset errors.
 | 
			
		||||
        mUrlView!!.error = null
 | 
			
		||||
        mLoginView!!.error = null
 | 
			
		||||
        mHTTPLoginView!!.error = null
 | 
			
		||||
        mPasswordView!!.error = null
 | 
			
		||||
        mHTTPPasswordView!!.error = null
 | 
			
		||||
        urlView.error = null
 | 
			
		||||
        loginView.error = null
 | 
			
		||||
        httpLoginView.error = null
 | 
			
		||||
        passwordView.error = null
 | 
			
		||||
        httpPasswordView.error = null
 | 
			
		||||
 | 
			
		||||
        // Store values at the time of the login attempt.
 | 
			
		||||
        val url = mUrlView!!.text.toString()
 | 
			
		||||
        val login = mLoginView!!.text.toString()
 | 
			
		||||
        val httpLogin = mHTTPLoginView!!.text.toString()
 | 
			
		||||
        val password = mPasswordView!!.text.toString()
 | 
			
		||||
        val httpPassword = mHTTPPasswordView!!.text.toString()
 | 
			
		||||
        val url = urlView.text.toString()
 | 
			
		||||
        val login = loginView.text.toString()
 | 
			
		||||
        val httpLogin = httpLoginView.text.toString()
 | 
			
		||||
        val password = passwordView.text.toString()
 | 
			
		||||
        val httpPassword = httpPasswordView.text.toString()
 | 
			
		||||
 | 
			
		||||
        var cancel = false
 | 
			
		||||
        var focusView: View? = null
 | 
			
		||||
 | 
			
		||||
        if (!isUrlValid(url)) {
 | 
			
		||||
            mUrlView!!.error = getString(R.string.login_url_problem)
 | 
			
		||||
            focusView = mUrlView
 | 
			
		||||
        if (!url.isBaseUrlValid(logErrors, this@LoginActivity)) {
 | 
			
		||||
            urlView.error = getString(R.string.login_url_problem)
 | 
			
		||||
            focusView = urlView
 | 
			
		||||
            cancel = true
 | 
			
		||||
            inValidCount++
 | 
			
		||||
            if (inValidCount == 3) {
 | 
			
		||||
                val alertDialog = AlertDialog.Builder(this).create()
 | 
			
		||||
                alertDialog.setTitle(getString(R.string.warning_wrong_url))
 | 
			
		||||
                alertDialog.setMessage(getString(R.string.text_wrong_url))
 | 
			
		||||
                alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
 | 
			
		||||
                        { dialog, _ -> dialog.dismiss() })
 | 
			
		||||
                alertDialog.setButton(
 | 
			
		||||
                    AlertDialog.BUTTON_NEUTRAL,
 | 
			
		||||
                    "OK",
 | 
			
		||||
                    { dialog, _ -> dialog.dismiss() }
 | 
			
		||||
                )
 | 
			
		||||
                alertDialog.show()
 | 
			
		||||
                inValidCount = 0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isWithLogin || isWithHTTPLogin) {
 | 
			
		||||
        if (isWithLogin) {
 | 
			
		||||
            if (TextUtils.isEmpty(password)) {
 | 
			
		||||
                mPasswordView!!.error = getString(R.string.error_invalid_password)
 | 
			
		||||
                focusView = mPasswordView
 | 
			
		||||
                passwordView.error = getString(R.string.error_invalid_password)
 | 
			
		||||
                focusView = passwordView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (TextUtils.isEmpty(login)) {
 | 
			
		||||
                mLoginView!!.error = getString(R.string.error_field_required)
 | 
			
		||||
                focusView = mLoginView
 | 
			
		||||
                loginView.error = getString(R.string.error_field_required)
 | 
			
		||||
                focusView = loginView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isWithHTTPLogin) {
 | 
			
		||||
            if (TextUtils.isEmpty(httpPassword)) {
 | 
			
		||||
                httpPasswordView.error = getString(R.string.error_invalid_password)
 | 
			
		||||
                focusView = httpPasswordView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (TextUtils.isEmpty(httpLogin)) {
 | 
			
		||||
                httpLoginView.error = getString(R.string.error_field_required)
 | 
			
		||||
                focusView = httpLoginView
 | 
			
		||||
                cancel = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (cancel) {
 | 
			
		||||
            focusView!!.requestFocus()
 | 
			
		||||
            focusView?.requestFocus()
 | 
			
		||||
        } else {
 | 
			
		||||
            showProgress(true)
 | 
			
		||||
 | 
			
		||||
            val editor = settings!!.edit()
 | 
			
		||||
            editor.putString("url", url)
 | 
			
		||||
            editor.putString("login", login)
 | 
			
		||||
            editor.putString("httpUserName", httpLogin)
 | 
			
		||||
            editor.putString("password", password)
 | 
			
		||||
            editor.putString("httpPassword", httpPassword)
 | 
			
		||||
            editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
 | 
			
		||||
            editor.apply()
 | 
			
		||||
 | 
			
		||||
            val api = SelfossApi(this@LoginActivity)
 | 
			
		||||
            api.login().enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                private fun preferenceError() {
 | 
			
		||||
                    editor.remove("url")
 | 
			
		||||
                    editor.remove("login")
 | 
			
		||||
                    editor.remove("httpUserName")
 | 
			
		||||
                    editor.remove("password")
 | 
			
		||||
                    editor.remove("httpPassword")
 | 
			
		||||
                    editor.apply()
 | 
			
		||||
                    mUrlView!!.error = getString(R.string.wrong_infos)
 | 
			
		||||
                    mLoginView!!.error = getString(R.string.wrong_infos)
 | 
			
		||||
                    mPasswordView!!.error = getString(R.string.wrong_infos)
 | 
			
		||||
                    mHTTPLoginView!!.error = getString(R.string.wrong_infos)
 | 
			
		||||
                    mHTTPPasswordView!!.error = getString(R.string.wrong_infos)
 | 
			
		||||
                    showProgress(false)
 | 
			
		||||
                }
 | 
			
		||||
            val api = SelfossApi(
 | 
			
		||||
                this,
 | 
			
		||||
                this@LoginActivity,
 | 
			
		||||
                isWithSelfSignedCert,
 | 
			
		||||
                -1L,
 | 
			
		||||
                isWithSelfSignedCert
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
                override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
 | 
			
		||||
                    if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                        mFirebaseAnalytics!!.logEvent(FirebaseAnalytics.Event.LOGIN, Bundle())
 | 
			
		||||
                        goToMain()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        preferenceError()
 | 
			
		||||
            if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
 | 
			
		||||
                api.login().enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                    private fun preferenceError(t: Throwable) {
 | 
			
		||||
                        editor.remove("url")
 | 
			
		||||
                        editor.remove("login")
 | 
			
		||||
                        editor.remove("httpUserName")
 | 
			
		||||
                        editor.remove("password")
 | 
			
		||||
                        editor.remove("httpPassword")
 | 
			
		||||
                        editor.apply()
 | 
			
		||||
                        urlView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        loginView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        passwordView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        httpLoginView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        httpPasswordView.error = getString(R.string.wrong_infos)
 | 
			
		||||
                        if (logErrors) {
 | 
			
		||||
                            ACRA.getErrorReporter().maybeHandleSilentException(t, this@LoginActivity)
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                this@LoginActivity,
 | 
			
		||||
                                t.message,
 | 
			
		||||
                                Toast.LENGTH_LONG
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                        showProgress(false)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                    preferenceError()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
                    override fun onResponse(
 | 
			
		||||
                        call: Call<SuccessResponse>,
 | 
			
		||||
                        response: Response<SuccessResponse>
 | 
			
		||||
                    ) {
 | 
			
		||||
                        if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                            goToMain()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            preferenceError(Exception("No response body..."))
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                        preferenceError(t)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                showProgress(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Shows the progress UI and hides the login form.
 | 
			
		||||
     */
 | 
			
		||||
    private fun showProgress(show: Boolean) {
 | 
			
		||||
        val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
 | 
			
		||||
 | 
			
		||||
        mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
        mLoginFormView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
 | 
			
		||||
                if (show) 0F else 1F).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
        loginForm.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
        loginForm
 | 
			
		||||
            .animate()
 | 
			
		||||
            .setDuration(shortAnimTime.toLong())
 | 
			
		||||
            .alpha(
 | 
			
		||||
                if (show) 0F else 1F
 | 
			
		||||
            ).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
            override fun onAnimationEnd(animation: Animator) {
 | 
			
		||||
                mLoginFormView!!.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
                loginForm.visibility = if (show) View.GONE else View.VISIBLE
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
        mProgressView!!.animate().setDuration(shortAnimTime.toLong()).alpha(
 | 
			
		||||
                if (show) 1F else 0F).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
        loginProgress.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
        loginProgress
 | 
			
		||||
            .animate()
 | 
			
		||||
            .setDuration(shortAnimTime.toLong())
 | 
			
		||||
            .alpha(
 | 
			
		||||
                if (show) 1F else 0F
 | 
			
		||||
            ).setListener(object : AnimatorListenerAdapter() {
 | 
			
		||||
            override fun onAnimationEnd(animation: Animator) {
 | 
			
		||||
                mProgressView!!.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
                loginProgress.visibility = if (show) View.VISIBLE else View.GONE
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
			
		||||
        val inflater = menuInflater
 | 
			
		||||
        inflater.inflate(R.menu.login_menu, menu)
 | 
			
		||||
        menuInflater.inflate(R.menu.login_menu, menu)
 | 
			
		||||
        menu.findItem(R.id.login_debug).isChecked = logErrors
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -251,10 +298,18 @@ class LoginActivity : AppCompatActivity() {
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            R.id.about -> {
 | 
			
		||||
                LibsBuilder()
 | 
			
		||||
                        .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
 | 
			
		||||
                        .withAboutIconShown(true)
 | 
			
		||||
                        .withAboutVersionShown(true)
 | 
			
		||||
                        .start(this)
 | 
			
		||||
                    .withActivityStyle(Libs.ActivityStyle.LIGHT_DARK_TOOLBAR)
 | 
			
		||||
                    .withAboutIconShown(true)
 | 
			
		||||
                    .withAboutVersionShown(true)
 | 
			
		||||
                    .start(this)
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            R.id.login_debug -> {
 | 
			
		||||
                val newState = !item.isChecked
 | 
			
		||||
                item.isChecked = newState
 | 
			
		||||
                logErrors = newState
 | 
			
		||||
                editor.putBoolean("login_debug", newState)
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            else -> return super.onOptionsItemSelected(item)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,7 @@ package apps.amine.bou.readerforselfoss
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
 | 
			
		||||
class MainActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
@@ -12,15 +11,9 @@ class MainActivity : AppCompatActivity() {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        setContentView(R.layout.activity_main)
 | 
			
		||||
 | 
			
		||||
        if (PreferenceManager.getDefaultSharedPreferences(baseContext).getBoolean("firstStart", true)) {
 | 
			
		||||
            val i = Intent(this@MainActivity, IntroActivity::class.java)
 | 
			
		||||
            startActivity(i)
 | 
			
		||||
        } else {
 | 
			
		||||
            val intent = Intent(this, LoginActivity::class.java)
 | 
			
		||||
            startActivity(intent)
 | 
			
		||||
        }
 | 
			
		||||
        val intent = Intent(this, LoginActivity::class.java)
 | 
			
		||||
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        finish()
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,50 +1,144 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.app.NotificationChannel
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.multidex.MultiDexApplication
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.multidex.MultiDexApplication
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import com.crashlytics.android.Crashlytics
 | 
			
		||||
import com.github.stkent.amplify.tracking.Amplify
 | 
			
		||||
import io.fabric.sdk.android.Fabric
 | 
			
		||||
import com.anupcowkur.reservoir.Reservoir
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.mikepenz.iconics.IconicsDrawable
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import com.github.stkent.amplify.feedback.DefaultEmailFeedbackCollector
 | 
			
		||||
import com.github.stkent.amplify.feedback.GooglePlayStoreFeedbackCollector
 | 
			
		||||
import com.github.stkent.amplify.tracking.Amplify
 | 
			
		||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
 | 
			
		||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import org.acra.ReportField
 | 
			
		||||
import org.acra.annotation.AcraCore
 | 
			
		||||
import org.acra.annotation.AcraDialog
 | 
			
		||||
import org.acra.annotation.AcraHttpSender
 | 
			
		||||
import org.acra.sender.HttpSender
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.util.UUID.randomUUID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@AcraHttpSender(uri = "http://37.187.110.167/amine/acra/simplest-acra.php",
 | 
			
		||||
                httpMethod = HttpSender.Method.POST)
 | 
			
		||||
@AcraDialog(resText = R.string.crash_dialog_text,
 | 
			
		||||
            resCommentPrompt = R.string.crash_dialog_comment,
 | 
			
		||||
            resTheme = android.R.style.Theme_DeviceDefault_Dialog)
 | 
			
		||||
@AcraCore(reportContent = [ReportField.REPORT_ID, ReportField.INSTALLATION_ID,
 | 
			
		||||
    ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME,
 | 
			
		||||
    ReportField.BUILD, ReportField.ANDROID_VERSION, ReportField.BRAND, ReportField.PHONE_MODEL,
 | 
			
		||||
    ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
 | 
			
		||||
    ReportField.STACK_TRACE, ReportField.APPLICATION_LOG, ReportField.LOGCAT,
 | 
			
		||||
    ReportField.INITIAL_CONFIGURATION, ReportField.CRASH_CONFIGURATION, ReportField.IS_SILENT,
 | 
			
		||||
    ReportField.USER_APP_START_DATE, ReportField.USER_COMMENT, ReportField.USER_CRASH_DATE, ReportField.USER_EMAIL, ReportField.CUSTOM_DATA],
 | 
			
		||||
          buildConfigClass = BuildConfig::class)
 | 
			
		||||
class MyApp : MultiDexApplication() {
 | 
			
		||||
 | 
			
		||||
    override fun onCreate() {
 | 
			
		||||
        super.onCreate()
 | 
			
		||||
        if (!BuildConfig.DEBUG)
 | 
			
		||||
            Fabric.with(this, Crashlytics())
 | 
			
		||||
 | 
			
		||||
        Amplify.initSharedInstance(this)
 | 
			
		||||
                .setFeedbackEmailAddress(getString(R.string.feedback_email))
 | 
			
		||||
                .setAlwaysShow(BuildConfig.DEBUG)
 | 
			
		||||
                .applyAllDefaultRules()
 | 
			
		||||
        initAmplify()
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            Reservoir.init(this, 8192) //in bytes
 | 
			
		||||
        } catch (e: IOException) {
 | 
			
		||||
            //failure
 | 
			
		||||
        val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
        if (prefs.getString("unique_id", "").isEmpty()) {
 | 
			
		||||
            val editor = prefs.edit()
 | 
			
		||||
            editor.putString("unique_id", randomUUID().toString())
 | 
			
		||||
            editor.apply()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        initDrawerImageLoader()
 | 
			
		||||
 | 
			
		||||
        initTheme()
 | 
			
		||||
 | 
			
		||||
        tryToHandleBug()
 | 
			
		||||
 | 
			
		||||
        handleNotificationChannels()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleNotificationChannels() {
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 | 
			
		||||
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
 | 
			
		||||
 | 
			
		||||
            val name = getString(R.string.notification_channel_sync)
 | 
			
		||||
            val importance = NotificationManager.IMPORTANCE_LOW
 | 
			
		||||
            val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
 | 
			
		||||
 | 
			
		||||
            val newItemsChannelname = getString(R.string.new_items_channel_sync)
 | 
			
		||||
            val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
 | 
			
		||||
            val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
 | 
			
		||||
 | 
			
		||||
            notificationManager.createNotificationChannel(mChannel)
 | 
			
		||||
            notificationManager.createNotificationChannel(newItemsChannelmChannel)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun attachBaseContext(base: Context?) {
 | 
			
		||||
        super.attachBaseContext(base)
 | 
			
		||||
        val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
        ACRA.init(this)
 | 
			
		||||
        ACRA.getErrorReporter().putCustomData("unique_id", prefs.getString("unique_id", ""))
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initAmplify() {
 | 
			
		||||
        Amplify.initSharedInstance(this)
 | 
			
		||||
            .setPositiveFeedbackCollectors(GooglePlayStoreFeedbackCollector())
 | 
			
		||||
            .setCriticalFeedbackCollectors(DefaultEmailFeedbackCollector(Config.feedbackEmail))
 | 
			
		||||
            .applyAllDefaultRules()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initDrawerImageLoader() {
 | 
			
		||||
        DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
 | 
			
		||||
            override fun set(imageView: ImageView?, uri: Uri?, placeholder: Drawable?, tag: String?) {
 | 
			
		||||
                Glide.with(imageView?.context).load(uri).placeholder(placeholder).into(imageView)
 | 
			
		||||
            override fun set(
 | 
			
		||||
                imageView: ImageView?,
 | 
			
		||||
                uri: Uri?,
 | 
			
		||||
                placeholder: Drawable?,
 | 
			
		||||
                tag: String?
 | 
			
		||||
            ) {
 | 
			
		||||
                Glide.with(imageView?.context)
 | 
			
		||||
                    .load(uri)
 | 
			
		||||
                    .apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
 | 
			
		||||
                    .into(imageView)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun cancel(imageView: ImageView?) {
 | 
			
		||||
                Glide.clear(imageView)
 | 
			
		||||
                Glide.with(imageView?.context).clear(imageView)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun placeholder(ctx: Context?, tag: String?): Drawable {
 | 
			
		||||
                return applicationContext.resources.getDrawable(R.mipmap.ic_launcher)
 | 
			
		||||
                return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun initTheme() {
 | 
			
		||||
        Scoop.waffleCone()
 | 
			
		||||
            .addFlavor(getString(R.string.default_theme), R.style.NoBar, true)
 | 
			
		||||
            .addFlavor(getString(R.string.default_dark_theme), R.style.NoBarDark, false)
 | 
			
		||||
            .setSharedPreferences(PreferenceManager.getDefaultSharedPreferences(this))
 | 
			
		||||
            .initialize()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun tryToHandleBug() {
 | 
			
		||||
        val oldHandler = Thread.getDefaultUncaughtExceptionHandler()
 | 
			
		||||
 | 
			
		||||
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
 | 
			
		||||
            if (e is java.lang.NoClassDefFoundError && e.stackTrace.asList().any {
 | 
			
		||||
                    it.toString().contains("android.view.ViewDebug")
 | 
			
		||||
                }) {
 | 
			
		||||
                Unit
 | 
			
		||||
            } else {
 | 
			
		||||
                oldHandler.uncaughtException(thread, e)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,105 +1,364 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.fragment.app.FragmentManager
 | 
			
		||||
import androidx.fragment.app.FragmentStatePagerAdapter
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.viewpager.widget.ViewPager
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import android.view.Menu
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.ImageButton
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import org.sufficientlysecure.htmltextview.HtmlHttpImageGetter
 | 
			
		||||
import org.sufficientlysecure.htmltextview.HtmlTextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.room.Room
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.succeeded
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toggleStar
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_reader.*
 | 
			
		||||
import me.relex.circleindicator.CircleIndicator
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import xyz.klinker.android.drag_dismiss.activity.DragDismissActivity
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class ReaderActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
class ReaderActivity : DragDismissActivity() {
 | 
			
		||||
    private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
 | 
			
		||||
    private var markOnScroll: Boolean = false
 | 
			
		||||
    private var debugReadingItems: Boolean = false
 | 
			
		||||
    private var currentItem: Int = 0
 | 
			
		||||
    private lateinit var userIdentifier: String
 | 
			
		||||
 | 
			
		||||
    override fun onStart() {
 | 
			
		||||
        super.onStart()
 | 
			
		||||
        mCustomTabActivityHelper!!.bindCustomTabsService(this)
 | 
			
		||||
    private lateinit var api: SelfossApi
 | 
			
		||||
 | 
			
		||||
    private lateinit var toolbarMenu: Menu
 | 
			
		||||
 | 
			
		||||
    private lateinit var db: AppDatabase
 | 
			
		||||
    private lateinit var prefs: SharedPreferences
 | 
			
		||||
 | 
			
		||||
    private var activeAlignment: Int = 1
 | 
			
		||||
    val JUSTIFY = 1
 | 
			
		||||
    val ALIGN_LEFT = 2
 | 
			
		||||
 | 
			
		||||
    private fun showMenuItem(willAddToFavorite: Boolean) {
 | 
			
		||||
        toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite
 | 
			
		||||
        toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        mCustomTabActivityHelper!!.unbindCustomTabsService(this)
 | 
			
		||||
    private fun canFavorite() {
 | 
			
		||||
        showMenuItem(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateContent(inflater: LayoutInflater, parent: ViewGroup, savedInstanceState: Bundle?): View {
 | 
			
		||||
        val v = inflater.inflate(R.layout.activity_reader, parent, false)
 | 
			
		||||
        showProgressBar()
 | 
			
		||||
    private fun canRemoveFromFavorite() {
 | 
			
		||||
        showMenuItem(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        val image = v.findViewById(R.id.imageView) as ImageView
 | 
			
		||||
        val source = v.findViewById(R.id.source) as TextView
 | 
			
		||||
        val title = v.findViewById(R.id.title) as TextView
 | 
			
		||||
        val content = v.findViewById(R.id.content) as HtmlTextView
 | 
			
		||||
        val url = intent.getStringExtra("url")
 | 
			
		||||
        val parser = MercuryApi(getString(R.string.mercury))
 | 
			
		||||
        val browserBtn: ImageButton = v.findViewById(R.id.browserBtn) as ImageButton
 | 
			
		||||
        val shareBtn: ImageButton = v.findViewById(R.id.shareBtn) as ImageButton
 | 
			
		||||
    private lateinit var editor: SharedPreferences.Editor
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        val customTabsIntent = buildCustomTabsIntent(this@ReaderActivity)
 | 
			
		||||
        mCustomTabActivityHelper = CustomTabActivityHelper()
 | 
			
		||||
        mCustomTabActivityHelper!!.bindCustomTabsService(this)
 | 
			
		||||
        setContentView(R.layout.activity_reader)
 | 
			
		||||
 | 
			
		||||
        db = Room.databaseBuilder(
 | 
			
		||||
            applicationContext,
 | 
			
		||||
            AppDatabase::class.java, "selfoss-database"
 | 
			
		||||
        ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
 | 
			
		||||
 | 
			
		||||
        parser.parseUrl(url).enqueue(object : Callback<ParsedContent> {
 | 
			
		||||
            override fun onResponse(call: Call<ParsedContent>, response: Response<ParsedContent>) {
 | 
			
		||||
                if (response.body() != null && response.body()!!.content != null && response.body()!!.content.isNotEmpty()) {
 | 
			
		||||
                    source.text = response.body()!!.domain
 | 
			
		||||
                    title.text = response.body()!!.title
 | 
			
		||||
                    if (response.body()!!.content != null && !response.body()!!.content.isEmpty())
 | 
			
		||||
                        content.setHtml(response.body()!!.content, HtmlHttpImageGetter(content, null, true))
 | 
			
		||||
                    if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isEmpty())
 | 
			
		||||
                        Glide.with(applicationContext).load(response.body()!!.lead_image_url).asBitmap().fitCenter().into(image)
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, toolBar)
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
                    shareBtn.setOnClickListener {
 | 
			
		||||
                        val sendIntent = Intent()
 | 
			
		||||
                        sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                        sendIntent.action = Intent.ACTION_SEND
 | 
			
		||||
                        sendIntent.putExtra(Intent.EXTRA_TEXT, response.body()!!.url)
 | 
			
		||||
                        sendIntent.type = "text/plain"
 | 
			
		||||
                        startActivity(Intent.createChooser(sendIntent, getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
 | 
			
		||||
        setSupportActionBar(toolBar)
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        prefs = PreferenceManager.getDefaultSharedPreferences(this)
 | 
			
		||||
        editor = prefs.edit()
 | 
			
		||||
 | 
			
		||||
        debugReadingItems = prefs.getBoolean("read_debug", false)
 | 
			
		||||
        userIdentifier = prefs.getString("unique_id", "")
 | 
			
		||||
        markOnScroll = prefs.getBoolean("mark_on_scroll", false)
 | 
			
		||||
        activeAlignment = prefs.getInt("text_align", JUSTIFY)
 | 
			
		||||
 | 
			
		||||
        api = SelfossApi(
 | 
			
		||||
            this,
 | 
			
		||||
            this@ReaderActivity,
 | 
			
		||||
            prefs.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
            prefs.getString("api_timeout", "-1").toLong(),
 | 
			
		||||
            prefs.getBoolean("should_log_everything", false)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (allItems.isEmpty()) {
 | 
			
		||||
            finish()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        currentItem = intent.getIntExtra("currentItem", 0)
 | 
			
		||||
 | 
			
		||||
        readItem(allItems[currentItem])
 | 
			
		||||
 | 
			
		||||
        pager.adapter =
 | 
			
		||||
                ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
 | 
			
		||||
        pager.currentItem = currentItem
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
 | 
			
		||||
        notifyAdapter()
 | 
			
		||||
 | 
			
		||||
        pager.setPageTransformer(true, DepthPageTransformer())
 | 
			
		||||
        (indicator as CircleIndicator).setViewPager(pager)
 | 
			
		||||
 | 
			
		||||
        pager.addOnPageChangeListener(
 | 
			
		||||
            object : ViewPager.SimpleOnPageChangeListener() {
 | 
			
		||||
 | 
			
		||||
                override fun onPageSelected(position: Int) {
 | 
			
		||||
 | 
			
		||||
                    if (allItems[position].starred) {
 | 
			
		||||
                        canRemoveFromFavorite()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        canFavorite()
 | 
			
		||||
                    }
 | 
			
		||||
                    readItem(allItems[pager.currentItem])
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
                    browserBtn.setOnClickListener {
 | 
			
		||||
                        val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
                        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                        intent.data = Uri.parse(response.body()!!.url)
 | 
			
		||||
                        startActivity(intent)
 | 
			
		||||
    fun readItem(item: Item) {
 | 
			
		||||
        if (markOnScroll) {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.itemsDao().delete(item.toEntity())
 | 
			
		||||
            }
 | 
			
		||||
            if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
 | 
			
		||||
                api.markItem(item.id).enqueue(
 | 
			
		||||
                    object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(
 | 
			
		||||
                            call: Call<SuccessResponse>,
 | 
			
		||||
                            response: Response<SuccessResponse>
 | 
			
		||||
                        ) {
 | 
			
		||||
                            if (!response.succeeded() && debugReadingItems) {
 | 
			
		||||
                                val message =
 | 
			
		||||
                                    "message: ${response.message()} " +
 | 
			
		||||
                                            "response isSuccess: ${response.isSuccessful} " +
 | 
			
		||||
                                            "response code: ${response.code()} " +
 | 
			
		||||
                                            "response message: ${response.message()} " +
 | 
			
		||||
                                            "response errorBody: ${response.errorBody()?.string()} " +
 | 
			
		||||
                                            "body success: ${response.body()?.success} " +
 | 
			
		||||
                                            "body isSuccess: ${response.body()?.isSuccess}"
 | 
			
		||||
                                ACRA.getErrorReporter()
 | 
			
		||||
                                    .maybeHandleSilentException(Exception(message), this@ReaderActivity)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(
 | 
			
		||||
                            call: Call<SuccessResponse>,
 | 
			
		||||
                            t: Throwable
 | 
			
		||||
                        ) {
 | 
			
		||||
                            thread {
 | 
			
		||||
                                db.itemsDao().insertAllItems(item.toEntity())
 | 
			
		||||
                            }
 | 
			
		||||
                            if (debugReadingItems) {
 | 
			
		||||
                                ACRA.getErrorReporter()
 | 
			
		||||
                                    .maybeHandleSilentException(t, this@ReaderActivity)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                thread {
 | 
			
		||||
                    db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
                    hideProgressBar()
 | 
			
		||||
    private fun notifyAdapter() {
 | 
			
		||||
        (pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPause() {
 | 
			
		||||
        super.onPause()
 | 
			
		||||
        if (markOnScroll) {
 | 
			
		||||
            pager.clearOnPageChangeListeners()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onSaveInstanceState(oldInstanceState: Bundle?) {
 | 
			
		||||
        super.onSaveInstanceState(oldInstanceState)
 | 
			
		||||
        oldInstanceState!!.clear()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
 | 
			
		||||
        FragmentStatePagerAdapter(fm) {
 | 
			
		||||
 | 
			
		||||
        override fun getCount(): Int {
 | 
			
		||||
            return allItems.size
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun getItem(position: Int): ArticleFragment {
 | 
			
		||||
            return ArticleFragment.newInstance(position, allItems)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun startUpdate(container: ViewGroup) {
 | 
			
		||||
            super.startUpdate(container)
 | 
			
		||||
 | 
			
		||||
            container.background = ColorDrawable(
 | 
			
		||||
                ContextCompat.getColor(
 | 
			
		||||
                    this@ReaderActivity,
 | 
			
		||||
                    appColors.colorBackground
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun alignmentMenu(showJustify: Boolean) {
 | 
			
		||||
        toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify
 | 
			
		||||
        toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
 | 
			
		||||
        val inflater = menuInflater
 | 
			
		||||
        inflater.inflate(R.menu.reader_menu, menu)
 | 
			
		||||
        toolbarMenu = menu
 | 
			
		||||
 | 
			
		||||
        if (!allItems.isEmpty() && allItems[currentItem].starred) {
 | 
			
		||||
            canRemoveFromFavorite()
 | 
			
		||||
        } else {
 | 
			
		||||
            canFavorite()
 | 
			
		||||
        }
 | 
			
		||||
        if (activeAlignment == JUSTIFY) {
 | 
			
		||||
            alignmentMenu(false)
 | 
			
		||||
        } else {
 | 
			
		||||
            alignmentMenu(true)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
 | 
			
		||||
        fun afterSave() {
 | 
			
		||||
            allItems[pager.currentItem] =
 | 
			
		||||
                    allItems[pager.currentItem].toggleStar()
 | 
			
		||||
            notifyAdapter()
 | 
			
		||||
            canRemoveFromFavorite()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun afterUnsave() {
 | 
			
		||||
            allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
 | 
			
		||||
            notifyAdapter()
 | 
			
		||||
            canFavorite()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        when (item.itemId) {
 | 
			
		||||
            android.R.id.home -> {
 | 
			
		||||
                onBackPressed()
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
            R.id.save -> {
 | 
			
		||||
                if (this@ReaderActivity.isNetworkAccessible(null)) {
 | 
			
		||||
                    api.starrItem(allItems[pager.currentItem].id)
 | 
			
		||||
                        .enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                            override fun onResponse(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                response: Response<SuccessResponse>
 | 
			
		||||
                            ) {
 | 
			
		||||
                                afterSave()
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            override fun onFailure(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                t: Throwable
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    baseContext,
 | 
			
		||||
                                    R.string.cant_mark_favortie,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                } else {
 | 
			
		||||
                    errorAfterMercuryCall()
 | 
			
		||||
                    thread {
 | 
			
		||||
                        db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
 | 
			
		||||
                        afterSave()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            R.id.unsave -> {
 | 
			
		||||
                if (this@ReaderActivity.isNetworkAccessible(null)) {
 | 
			
		||||
                    api.unstarrItem(allItems[pager.currentItem].id)
 | 
			
		||||
                        .enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                            override fun onResponse(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                response: Response<SuccessResponse>
 | 
			
		||||
                            ) {
 | 
			
		||||
                                afterUnsave()
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<ParsedContent>, t: Throwable) {
 | 
			
		||||
                errorAfterMercuryCall()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            private fun errorAfterMercuryCall() {
 | 
			
		||||
                CustomTabActivityHelper.openCustomTab(this@ReaderActivity, customTabsIntent, Uri.parse(url)
 | 
			
		||||
                ) { _, uri ->
 | 
			
		||||
                    val intent = Intent(Intent.ACTION_VIEW, uri)
 | 
			
		||||
                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                    startActivity(intent)
 | 
			
		||||
                            override fun onFailure(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                t: Throwable
 | 
			
		||||
                            ) {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    baseContext,
 | 
			
		||||
                                    R.string.cant_unmark_favortie,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                } else {
 | 
			
		||||
                    thread {
 | 
			
		||||
                        db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
 | 
			
		||||
                        afterUnsave()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                finish()
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        return v
 | 
			
		||||
            R.id.align_left -> {
 | 
			
		||||
                editor.putInt("text_align", ALIGN_LEFT)
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                alignmentMenu(true)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
            }
 | 
			
		||||
            R.id.align_justify -> {
 | 
			
		||||
                editor.putInt("text_align", JUSTIFY)
 | 
			
		||||
                editor.apply()
 | 
			
		||||
                alignmentMenu(false)
 | 
			
		||||
                refreshFragment()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return super.onOptionsItemSelected(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshFragment() {
 | 
			
		||||
        finish()
 | 
			
		||||
        overridePendingTransition(0, 0)
 | 
			
		||||
        startActivity(intent)
 | 
			
		||||
        overridePendingTransition(0, 0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        var allItems: ArrayList<Item> = ArrayList()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,56 +1,105 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss
 | 
			
		||||
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v7.app.AppCompatActivity
 | 
			
		||||
import android.support.v7.widget.LinearLayoutManager
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
 | 
			
		||||
import com.melnykov.fab.FloatingActionButton
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import com.ftinc.scoop.Scoop
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_sources.*
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SourcesActivity : AppCompatActivity() {
 | 
			
		||||
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(this@SourcesActivity)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.activity_sources)
 | 
			
		||||
 | 
			
		||||
        val scoop = Scoop.getInstance()
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.value, toolbar)
 | 
			
		||||
        if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(toolbar)
 | 
			
		||||
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
 | 
			
		||||
        supportActionBar?.setDisplayShowHomeEnabled(true)
 | 
			
		||||
 | 
			
		||||
        fab.rippleColor = appColors.colorAccentDark
 | 
			
		||||
        fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        recyclerView.clearOnScrollListeners()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        val mFab = findViewById(R.id.fab) as FloatingActionButton
 | 
			
		||||
        val mRecyclerView = findViewById(R.id.activity_sources) as RecyclerView
 | 
			
		||||
        val mLayoutManager = LinearLayoutManager(this)
 | 
			
		||||
        val api = SelfossApi(this)
 | 
			
		||||
        var items: ArrayList<Sources> = ArrayList()
 | 
			
		||||
 | 
			
		||||
        mFab.attachToRecyclerView(mRecyclerView)
 | 
			
		||||
        mRecyclerView.setHasFixedSize(true)
 | 
			
		||||
        mRecyclerView.layoutManager = mLayoutManager
 | 
			
		||||
        val prefs = PreferenceManager.getDefaultSharedPreferences(this)
 | 
			
		||||
 | 
			
		||||
        api.sources.enqueue(object : Callback<List<Sources>> {
 | 
			
		||||
            override fun onResponse(call: Call<List<Sources>>, response: Response<List<Sources>>) {
 | 
			
		||||
                if (response.body() != null && response.body()!!.isNotEmpty()) {
 | 
			
		||||
                    items = response.body() as ArrayList<Sources>
 | 
			
		||||
        val api = SelfossApi(
 | 
			
		||||
            this,
 | 
			
		||||
            this@SourcesActivity,
 | 
			
		||||
            prefs.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
            prefs.getString("api_timeout", "-1").toLong(),
 | 
			
		||||
            prefs.getBoolean("should_log_everything", false)
 | 
			
		||||
        )
 | 
			
		||||
        var items: ArrayList<Source> = ArrayList()
 | 
			
		||||
 | 
			
		||||
        recyclerView.setHasFixedSize(true)
 | 
			
		||||
        recyclerView.layoutManager = mLayoutManager
 | 
			
		||||
 | 
			
		||||
        if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
 | 
			
		||||
            api.sources.enqueue(object : Callback<List<Source>> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<List<Source>>,
 | 
			
		||||
                    response: Response<List<Source>>
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (response.body() != null && response.body()!!.isNotEmpty()) {
 | 
			
		||||
                        items = response.body() as ArrayList<Source>
 | 
			
		||||
                    }
 | 
			
		||||
                    val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
 | 
			
		||||
                    recyclerView.adapter = mAdapter
 | 
			
		||||
                    mAdapter.notifyDataSetChanged()
 | 
			
		||||
                    if (items.isEmpty()) {
 | 
			
		||||
                        Toast.makeText(
 | 
			
		||||
                            this@SourcesActivity,
 | 
			
		||||
                            R.string.nothing_here,
 | 
			
		||||
                            Toast.LENGTH_SHORT
 | 
			
		||||
                        ).show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
 | 
			
		||||
                mRecyclerView.adapter = mAdapter
 | 
			
		||||
                mAdapter.notifyDataSetChanged()
 | 
			
		||||
                if (items.isEmpty()) Toast.makeText(this@SourcesActivity, R.string.nothing_here, Toast.LENGTH_SHORT).show()
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<List<Sources>>, t: Throwable) {
 | 
			
		||||
                Toast.makeText(this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT).show()
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
                override fun onFailure(call: Call<List<Source>>, t: Throwable) {
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        this@SourcesActivity,
 | 
			
		||||
                        R.string.cant_get_sources,
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mFab.setOnClickListener {
 | 
			
		||||
        fab.setOnClickListener {
 | 
			
		||||
            startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,65 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.constraint.ConstraintLayout
 | 
			
		||||
import android.support.design.widget.Snackbar
 | 
			
		||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import androidx.cardview.widget.CardView
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.ImageButton
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import android.widget.ImageView.ScaleType
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.shareLink
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
 | 
			
		||||
import com.like.LikeButton
 | 
			
		||||
import com.like.OnLikeListener
 | 
			
		||||
import kotlinx.android.synthetic.main.card_item.view.*
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ItemCardAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
 | 
			
		||||
                      private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean,
 | 
			
		||||
                      private val articleViewer: Boolean, private val fullHeightCards: Boolean) : RecyclerView.Adapter<ItemCardAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.applicationContext
 | 
			
		||||
class ItemCardAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<Item>,
 | 
			
		||||
    override val api: SelfossApi,
 | 
			
		||||
    override val db: AppDatabase,
 | 
			
		||||
    private val helper: CustomTabActivityHelper,
 | 
			
		||||
    private val internalBrowser: Boolean,
 | 
			
		||||
    private val articleViewer: Boolean,
 | 
			
		||||
    private val fullHeightCards: Boolean,
 | 
			
		||||
    override val appColors: AppColors,
 | 
			
		||||
    override val debugReadingItems: Boolean,
 | 
			
		||||
    override val userIdentifier: String,
 | 
			
		||||
    override val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val imageMaxHeight: Int =
 | 
			
		||||
        c.resources.getDimension(R.dimen.card_image_max_height).toInt()
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as ConstraintLayout
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
 | 
			
		||||
        return ViewHolder(v)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -56,196 +67,141 @@ class ItemCardAdapter(private val app: Activity, private val items: ArrayList<It
 | 
			
		||||
        val itm = items[position]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        holder.saveBtn!!.isLiked = itm.starred
 | 
			
		||||
        holder.title!!.text = Html.fromHtml(itm.title)
 | 
			
		||||
        holder.mView.favButton.isLiked = itm.starred
 | 
			
		||||
        holder.mView.title.text = Html.fromHtml(itm.title)
 | 
			
		||||
        holder.mView.title.setOnTouchListener(LinkOnTouchListener())
 | 
			
		||||
 | 
			
		||||
        var sourceAndDate = itm.sourcetitle
 | 
			
		||||
        val d: Long
 | 
			
		||||
        try {
 | 
			
		||||
            d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
 | 
			
		||||
            sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
 | 
			
		||||
                    d,
 | 
			
		||||
                    Date().time,
 | 
			
		||||
                    DateUtils.MINUTE_IN_MILLIS,
 | 
			
		||||
                    DateUtils.FORMAT_ABBREV_RELATIVE
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: ParseException) {
 | 
			
		||||
            e.printStackTrace()
 | 
			
		||||
        holder.mView.title.setLinkTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
        holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
 | 
			
		||||
 | 
			
		||||
        if (!fullHeightCards) {
 | 
			
		||||
            holder.mView.itemImage.maxHeight = imageMaxHeight
 | 
			
		||||
            holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.sourceTitleAndDate!!.text = sourceAndDate
 | 
			
		||||
 | 
			
		||||
        if (itm.getThumbnail(c).isEmpty()) {
 | 
			
		||||
            Glide.clear(holder.itemImage)
 | 
			
		||||
            holder.itemImage!!.setImageDrawable(null)
 | 
			
		||||
            holder.mView.itemImage.visibility = View.GONE
 | 
			
		||||
            Glide.with(c).clear(holder.mView.itemImage)
 | 
			
		||||
            holder.mView.itemImage.setImageDrawable(null)
 | 
			
		||||
        } else {
 | 
			
		||||
            if (fullHeightCards) {
 | 
			
		||||
                Glide.with(c).load(itm.getThumbnail(c)).asBitmap().fitCenter().into(holder.itemImage)
 | 
			
		||||
            } else {
 | 
			
		||||
                Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.itemImage)
 | 
			
		||||
            }
 | 
			
		||||
            holder.mView.itemImage.visibility = View.VISIBLE
 | 
			
		||||
            c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val fHolder = holder
 | 
			
		||||
        if (itm.getIcon(c).isEmpty()) {
 | 
			
		||||
            val color = generator.getColor(itm.sourcetitle)
 | 
			
		||||
            val textDrawable = StringBuilder()
 | 
			
		||||
            for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
 | 
			
		||||
                textDrawable.append(s[0])
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val builder = TextDrawable.builder().round()
 | 
			
		||||
 | 
			
		||||
            val drawable = builder.build(textDrawable.toString(), color)
 | 
			
		||||
            holder.sourceImage!!.setImageDrawable(drawable)
 | 
			
		||||
            val drawable =
 | 
			
		||||
                TextDrawable
 | 
			
		||||
                    .builder()
 | 
			
		||||
                    .round()
 | 
			
		||||
                    .build(itm.sourcetitle.toTextDrawableString(c), color)
 | 
			
		||||
            holder.mView.sourceImage.setImageDrawable(drawable)
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
            Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
 | 
			
		||||
                override fun setResource(resource: Bitmap) {
 | 
			
		||||
                    val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
 | 
			
		||||
                    circularBitmapDrawable.isCircular = true
 | 
			
		||||
                    fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            c.circularBitmapDrawable(itm.getIcon(c), holder.mView.sourceImage)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.saveBtn!!.isLiked = itm.starred
 | 
			
		||||
        holder.mView.favButton.isLiked = itm.starred
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun doUnmark(i: Item, position: Int) {
 | 
			
		||||
        val s = Snackbar
 | 
			
		||||
                .make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
 | 
			
		||||
                .setAction(R.string.undo_string) {
 | 
			
		||||
                    items.add(position, i)
 | 
			
		||||
                    notifyItemInserted(position)
 | 
			
		||||
 | 
			
		||||
                    api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            items.remove(i)
 | 
			
		||||
                            notifyItemRemoved(position)
 | 
			
		||||
                            doUnmark(i, position)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun removeItemAtIndex(position: Int) {
 | 
			
		||||
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        items.remove(i)
 | 
			
		||||
        notifyItemRemoved(position)
 | 
			
		||||
 | 
			
		||||
        api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
            override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
 | 
			
		||||
 | 
			
		||||
                doUnmark(i, position)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
 | 
			
		||||
                items.add(i)
 | 
			
		||||
                notifyItemInserted(position)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
 | 
			
		||||
        var saveBtn: LikeButton? = null
 | 
			
		||||
        var browserBtn: ImageButton? = null
 | 
			
		||||
        var shareBtn: ImageButton? = null
 | 
			
		||||
        var itemImage: ImageView? = null
 | 
			
		||||
        var sourceImage: ImageView? = null
 | 
			
		||||
        var title: TextView? = null
 | 
			
		||||
        var sourceTitleAndDate: TextView? = null
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
 | 
			
		||||
        init {
 | 
			
		||||
            mView.setCardBackgroundColor(appColors.cardBackgroundColor)
 | 
			
		||||
            handleClickListeners()
 | 
			
		||||
            handleCustomTabActions()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleClickListeners() {
 | 
			
		||||
            sourceImage = mView.findViewById(R.id.sourceImage) as ImageView
 | 
			
		||||
            itemImage = mView.findViewById(R.id.itemImage) as ImageView
 | 
			
		||||
            title = mView.findViewById(R.id.title) as TextView
 | 
			
		||||
            sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
 | 
			
		||||
            saveBtn = mView.findViewById(R.id.favButton) as LikeButton
 | 
			
		||||
            shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
 | 
			
		||||
            browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
 | 
			
		||||
 | 
			
		||||
            if (!fullHeightCards) {
 | 
			
		||||
                itemImage!!.maxHeight = c.resources.getDimension(R.dimen.card_image_max_height).toInt()
 | 
			
		||||
                itemImage!!.scaleType = ScaleType.CENTER_CROP
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            saveBtn!!.setOnLikeListener(object : OnLikeListener {
 | 
			
		||||
            mView.favButton.setOnLikeListener(object : OnLikeListener {
 | 
			
		||||
                override fun liked(likeButton: LikeButton) {
 | 
			
		||||
                    val (id) = items[adapterPosition]
 | 
			
		||||
                    api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
                    if (c.isNetworkAccessible(null)) {
 | 
			
		||||
                        api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                            override fun onResponse(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                response: Response<SuccessResponse>
 | 
			
		||||
                            ) {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            saveBtn!!.isLiked = false
 | 
			
		||||
                            Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                            override fun onFailure(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                t: Throwable
 | 
			
		||||
                            ) {
 | 
			
		||||
                                mView.favButton.isLiked = false
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    c,
 | 
			
		||||
                                    R.string.cant_mark_favortie,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                    } else {
 | 
			
		||||
                        thread {
 | 
			
		||||
                            db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false))
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun unLiked(likeButton: LikeButton) {
 | 
			
		||||
                    val (id) = items[adapterPosition]
 | 
			
		||||
                    api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
                    if (c.isNetworkAccessible(null)) {
 | 
			
		||||
                        api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                            override fun onResponse(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                response: Response<SuccessResponse>
 | 
			
		||||
                            ) {
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            saveBtn!!.isLiked = true
 | 
			
		||||
                            Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                            override fun onFailure(
 | 
			
		||||
                                call: Call<SuccessResponse>,
 | 
			
		||||
                                t: Throwable
 | 
			
		||||
                            ) {
 | 
			
		||||
                                mView.favButton.isLiked = true
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    c,
 | 
			
		||||
                                    R.string.cant_unmark_favortie,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                    } else {
 | 
			
		||||
                        thread {
 | 
			
		||||
                            db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            shareBtn!!.setOnClickListener {
 | 
			
		||||
                val i = items[adapterPosition]
 | 
			
		||||
                val sendIntent = Intent()
 | 
			
		||||
                sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                sendIntent.action = Intent.ACTION_SEND
 | 
			
		||||
                sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
 | 
			
		||||
                sendIntent.type = "text/plain"
 | 
			
		||||
                c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
 | 
			
		||||
            mView.shareBtn.setOnClickListener {
 | 
			
		||||
                val item = items[adapterPosition]
 | 
			
		||||
                c.shareLink(item.getLinkDecoded(), item.title)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            browserBtn!!.setOnClickListener {
 | 
			
		||||
                val i = items[adapterPosition]
 | 
			
		||||
                val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                intent.data = Uri.parse(i.getLinkDecoded())
 | 
			
		||||
                c.startActivity(intent)
 | 
			
		||||
            mView.browserBtn.setOnClickListener {
 | 
			
		||||
                c.openInBrowserAsNewTask(items[adapterPosition])
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleCustomTabActions() {
 | 
			
		||||
            val customTabsIntent = buildCustomTabsIntent(c)
 | 
			
		||||
            val customTabsIntent = c.buildCustomTabsIntent()
 | 
			
		||||
            helper.bindCustomTabsService(app)
 | 
			
		||||
 | 
			
		||||
            mView.setOnClickListener {
 | 
			
		||||
                openItemUrl(items[adapterPosition],
 | 
			
		||||
                        customTabsIntent,
 | 
			
		||||
                        internalBrowser,
 | 
			
		||||
                        articleViewer,
 | 
			
		||||
                        app,
 | 
			
		||||
                        c)
 | 
			
		||||
                c.openItemUrl(
 | 
			
		||||
                    items,
 | 
			
		||||
                    adapterPosition,
 | 
			
		||||
                    items[adapterPosition].getLinkDecoded(),
 | 
			
		||||
                    customTabsIntent,
 | 
			
		||||
                    internalBrowser,
 | 
			
		||||
                    articleViewer,
 | 
			
		||||
                    app
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,53 +1,68 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.constraint.ConstraintLayout
 | 
			
		||||
import android.support.design.widget.Snackbar
 | 
			
		||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.text.Html
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import android.text.Spannable
 | 
			
		||||
import android.text.style.ClickableSpan
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.*
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.shareLink
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
 | 
			
		||||
import com.like.LikeButton
 | 
			
		||||
import com.like.OnLikeListener
 | 
			
		||||
import kotlinx.android.synthetic.main.list_item.view.*
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
import kotlin.collections.ArrayList
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ItemListAdapter(private val app: Activity, private val items: ArrayList<Item>, private val api: SelfossApi,
 | 
			
		||||
                      private val helper: CustomTabActivityHelper, private val clickBehavior: Boolean,
 | 
			
		||||
                      private val internalBrowser: Boolean, private val articleViewer: Boolean) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {
 | 
			
		||||
class ItemListAdapter(
 | 
			
		||||
    override val app: Activity,
 | 
			
		||||
    override var items: ArrayList<Item>,
 | 
			
		||||
    override val api: SelfossApi,
 | 
			
		||||
    override val db: AppDatabase,
 | 
			
		||||
    private val helper: CustomTabActivityHelper,
 | 
			
		||||
    private val internalBrowser: Boolean,
 | 
			
		||||
    private val articleViewer: Boolean,
 | 
			
		||||
    override val debugReadingItems: Boolean,
 | 
			
		||||
    override val userIdentifier: String,
 | 
			
		||||
    override val appColors: AppColors,
 | 
			
		||||
    override val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
    private val c: Context = app.applicationContext
 | 
			
		||||
    private val bars: ArrayList<Boolean> = ArrayList(Collections.nCopies(items.size + 1, false))
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(R.layout.list_item, parent, false) as ConstraintLayout
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(
 | 
			
		||||
            R.layout.list_item,
 | 
			
		||||
            parent,
 | 
			
		||||
            false
 | 
			
		||||
        ) as ConstraintLayout
 | 
			
		||||
        return ViewHolder(v)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -55,236 +70,74 @@ class ItemListAdapter(private val app: Activity, private val items: ArrayList<It
 | 
			
		||||
        val itm = items[position]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        holder.saveBtn!!.isLiked = itm.starred
 | 
			
		||||
        holder.title!!.text = Html.fromHtml(itm.title)
 | 
			
		||||
        holder.mView.title.text = Html.fromHtml(itm.title)
 | 
			
		||||
 | 
			
		||||
        var sourceAndDate = itm.sourcetitle
 | 
			
		||||
        val d: Long
 | 
			
		||||
        try {
 | 
			
		||||
            d = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(itm.datetime).time
 | 
			
		||||
            sourceAndDate += " " + DateUtils.getRelativeTimeSpanString(
 | 
			
		||||
                    d,
 | 
			
		||||
                    Date().time,
 | 
			
		||||
                    DateUtils.MINUTE_IN_MILLIS,
 | 
			
		||||
                    DateUtils.FORMAT_ABBREV_RELATIVE
 | 
			
		||||
            )
 | 
			
		||||
        } catch (e: ParseException) {
 | 
			
		||||
            e.printStackTrace()
 | 
			
		||||
        }
 | 
			
		||||
        holder.mView.title.setOnTouchListener(LinkOnTouchListener())
 | 
			
		||||
 | 
			
		||||
        holder.sourceTitleAndDate!!.text = sourceAndDate
 | 
			
		||||
        holder.mView.title.setLinkTextColor(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
        holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
 | 
			
		||||
 | 
			
		||||
        if (itm.getThumbnail(c).isEmpty()) {
 | 
			
		||||
            val sizeInInt = 46
 | 
			
		||||
            val sizeInDp = TypedValue.applyDimension(
 | 
			
		||||
                    TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
 | 
			
		||||
                    .displayMetrics).toInt()
 | 
			
		||||
                TypedValue.COMPLEX_UNIT_DIP, sizeInInt.toFloat(), c.resources
 | 
			
		||||
                    .displayMetrics
 | 
			
		||||
            ).toInt()
 | 
			
		||||
 | 
			
		||||
            val marginInInt = 16
 | 
			
		||||
            val marginInDp = TypedValue.applyDimension(
 | 
			
		||||
                    TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
 | 
			
		||||
                    .displayMetrics).toInt()
 | 
			
		||||
                TypedValue.COMPLEX_UNIT_DIP, marginInInt.toFloat(), c.resources
 | 
			
		||||
                    .displayMetrics
 | 
			
		||||
            ).toInt()
 | 
			
		||||
 | 
			
		||||
            val params = holder.sourceImage!!.layoutParams as ViewGroup.MarginLayoutParams
 | 
			
		||||
            val params = holder.mView.itemImage.layoutParams as ViewGroup.MarginLayoutParams
 | 
			
		||||
            params.height = sizeInDp
 | 
			
		||||
            params.width = sizeInDp
 | 
			
		||||
            params.setMargins(marginInDp, 0, 0, 0)
 | 
			
		||||
            holder.sourceImage!!.layoutParams = params
 | 
			
		||||
            holder.mView.itemImage.layoutParams = params
 | 
			
		||||
 | 
			
		||||
            if (itm.getIcon(c).isEmpty()) {
 | 
			
		||||
                val color = generator.getColor(itm.sourcetitle)
 | 
			
		||||
                val textDrawable = StringBuilder()
 | 
			
		||||
                for (s in itm.sourcetitle.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
 | 
			
		||||
                    textDrawable.append(s[0])
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                val builder = TextDrawable.builder().round()
 | 
			
		||||
                val drawable =
 | 
			
		||||
                    TextDrawable
 | 
			
		||||
                        .builder()
 | 
			
		||||
                        .round()
 | 
			
		||||
                        .build(itm.sourcetitle.toTextDrawableString(c), color)
 | 
			
		||||
 | 
			
		||||
                val drawable = builder.build(textDrawable.toString(), color)
 | 
			
		||||
                holder.sourceImage!!.setImageDrawable(drawable)
 | 
			
		||||
                holder.mView.itemImage.setImageDrawable(drawable)
 | 
			
		||||
            } else {
 | 
			
		||||
 | 
			
		||||
                val fHolder = holder
 | 
			
		||||
                Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
 | 
			
		||||
                    override fun setResource(resource: Bitmap) {
 | 
			
		||||
                        val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
 | 
			
		||||
                        circularBitmapDrawable.isCircular = true
 | 
			
		||||
                        fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Glide.with(c).load(itm.getThumbnail(c)).asBitmap().centerCrop().into(holder.sourceImage)
 | 
			
		||||
            c.bitmapCenterCrop(itm.getThumbnail(c), holder.mView.itemImage)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (bars[position]) {
 | 
			
		||||
            holder.actionBar!!.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            holder.actionBar!!.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.saveBtn!!.isLiked = itm.starred
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private fun doUnmark(i: Item, position: Int) {
 | 
			
		||||
        val s = Snackbar
 | 
			
		||||
                .make(app.findViewById(R.id.coordLayout), R.string.marked_as_read, Snackbar.LENGTH_LONG)
 | 
			
		||||
                .setAction(R.string.undo_string) {
 | 
			
		||||
                    items.add(position, i)
 | 
			
		||||
                    notifyItemInserted(position)
 | 
			
		||||
 | 
			
		||||
                    api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            items.remove(i)
 | 
			
		||||
                            notifyItemRemoved(position)
 | 
			
		||||
                            doUnmark(i, position)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv = view.findViewById(android.support.design.R.id.snackbar_text) as TextView
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun removeItemAtIndex(position: Int) {
 | 
			
		||||
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
 | 
			
		||||
        items.remove(i)
 | 
			
		||||
        notifyItemRemoved(position)
 | 
			
		||||
 | 
			
		||||
        api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
            override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
 | 
			
		||||
                doUnmark(i, position)
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                Toast.makeText(app, app.getString(R.string.cant_mark_read), Toast.LENGTH_SHORT).show()
 | 
			
		||||
                items.add(i)
 | 
			
		||||
                notifyItemInserted(position)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
 | 
			
		||||
        var saveBtn: LikeButton? = null
 | 
			
		||||
        var browserBtn: ImageButton? = null
 | 
			
		||||
        var shareBtn: ImageButton? = null
 | 
			
		||||
        var actionBar: RelativeLayout? = null
 | 
			
		||||
        var sourceImage: ImageView? = null
 | 
			
		||||
        var title: TextView? = null
 | 
			
		||||
        var sourceTitleAndDate: TextView? = null
 | 
			
		||||
 | 
			
		||||
        init {
 | 
			
		||||
            handleClickListeners()
 | 
			
		||||
            handleCustomTabActions()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleClickListeners() {
 | 
			
		||||
            actionBar = mView.findViewById(R.id.actionBar) as RelativeLayout
 | 
			
		||||
            sourceImage = mView.findViewById(R.id.itemImage) as ImageView
 | 
			
		||||
            title = mView.findViewById(R.id.title) as TextView
 | 
			
		||||
            sourceTitleAndDate = mView.findViewById(R.id.sourceTitleAndDate) as TextView
 | 
			
		||||
            saveBtn = mView.findViewById(R.id.favButton) as LikeButton
 | 
			
		||||
            shareBtn = mView.findViewById(R.id.shareBtn) as ImageButton
 | 
			
		||||
            browserBtn = mView.findViewById(R.id.browserBtn) as ImageButton
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            saveBtn!!.setOnLikeListener(object : OnLikeListener {
 | 
			
		||||
                override fun liked(likeButton: LikeButton) {
 | 
			
		||||
                    val (id) = items[adapterPosition]
 | 
			
		||||
                    api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            saveBtn!!.isLiked = false
 | 
			
		||||
                            Toast.makeText(c, R.string.cant_mark_favortie, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun unLiked(likeButton: LikeButton) {
 | 
			
		||||
                    val (id) = items[adapterPosition]
 | 
			
		||||
                    api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {}
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            saveBtn!!.isLiked = true
 | 
			
		||||
                            Toast.makeText(c, R.string.cant_unmark_favortie, Toast.LENGTH_SHORT).show()
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            shareBtn!!.setOnClickListener {
 | 
			
		||||
                val i = items[adapterPosition]
 | 
			
		||||
                val sendIntent = Intent()
 | 
			
		||||
                sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                sendIntent.action = Intent.ACTION_SEND
 | 
			
		||||
                sendIntent.putExtra(Intent.EXTRA_TEXT, i.getLinkDecoded())
 | 
			
		||||
                sendIntent.type = "text/plain"
 | 
			
		||||
                c.startActivity(Intent.createChooser(sendIntent, c.getString(R.string.share)).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            browserBtn!!.setOnClickListener {
 | 
			
		||||
                val i = items[adapterPosition]
 | 
			
		||||
                val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                intent.data = Uri.parse(i.getLinkDecoded())
 | 
			
		||||
                c.startActivity(intent)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        private fun handleCustomTabActions() {
 | 
			
		||||
            val customTabsIntent = buildCustomTabsIntent(c)
 | 
			
		||||
            val customTabsIntent = c.buildCustomTabsIntent()
 | 
			
		||||
            helper.bindCustomTabsService(app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            if (!clickBehavior) {
 | 
			
		||||
                mView.setOnClickListener {
 | 
			
		||||
                    openItemUrl(items[adapterPosition],
 | 
			
		||||
                            customTabsIntent,
 | 
			
		||||
                            internalBrowser,
 | 
			
		||||
                            articleViewer,
 | 
			
		||||
                            app,
 | 
			
		||||
                            c)
 | 
			
		||||
                }
 | 
			
		||||
                mView.setOnLongClickListener {
 | 
			
		||||
                    actionBarShowHide()
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                mView.setOnClickListener { actionBarShowHide() }
 | 
			
		||||
                mView.setOnLongClickListener {
 | 
			
		||||
                    openItemUrl(items[adapterPosition],
 | 
			
		||||
                            customTabsIntent,
 | 
			
		||||
                            internalBrowser,
 | 
			
		||||
                            articleViewer,
 | 
			
		||||
                            app,
 | 
			
		||||
                            c)
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            mView.setOnClickListener {
 | 
			
		||||
                c.openItemUrl(
 | 
			
		||||
                    items,
 | 
			
		||||
                    adapterPosition,
 | 
			
		||||
                    items[adapterPosition].getLinkDecoded(),
 | 
			
		||||
                    customTabsIntent,
 | 
			
		||||
                    internalBrowser,
 | 
			
		||||
                    articleViewer,
 | 
			
		||||
                    app
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun actionBarShowHide() {
 | 
			
		||||
            bars[adapterPosition] = true
 | 
			
		||||
            if (actionBar!!.visibility == View.GONE)
 | 
			
		||||
                actionBar!!.visibility = View.VISIBLE
 | 
			
		||||
            else
 | 
			
		||||
                actionBar!!.visibility = View.GONE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,161 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.succeeded
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
 | 
			
		||||
    abstract var items: ArrayList<Item>
 | 
			
		||||
    abstract val api: SelfossApi
 | 
			
		||||
    abstract val db: AppDatabase
 | 
			
		||||
    abstract val debugReadingItems: Boolean
 | 
			
		||||
    abstract val userIdentifier: String
 | 
			
		||||
    abstract val app: Activity
 | 
			
		||||
    abstract val appColors: AppColors
 | 
			
		||||
    abstract val updateItems: (ArrayList<Item>) -> Unit
 | 
			
		||||
 | 
			
		||||
    fun updateAllItems(newItems: ArrayList<Item>) {
 | 
			
		||||
        items = newItems
 | 
			
		||||
        notifyDataSetChanged()
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun doUnmark(i: Item, position: Int) {
 | 
			
		||||
        val s = Snackbar
 | 
			
		||||
            .make(
 | 
			
		||||
                app.findViewById(R.id.coordLayout),
 | 
			
		||||
                R.string.marked_as_read,
 | 
			
		||||
                Snackbar.LENGTH_LONG
 | 
			
		||||
            )
 | 
			
		||||
            .setAction(R.string.undo_string) {
 | 
			
		||||
                items.add(position, i)
 | 
			
		||||
                thread {
 | 
			
		||||
                    db.itemsDao().insertAllItems(i.toEntity())
 | 
			
		||||
                }
 | 
			
		||||
                notifyItemInserted(position)
 | 
			
		||||
                updateItems(items)
 | 
			
		||||
 | 
			
		||||
                if (app.isNetworkAccessible(null)) {
 | 
			
		||||
                    api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(
 | 
			
		||||
                            call: Call<SuccessResponse>,
 | 
			
		||||
                            response: Response<SuccessResponse>
 | 
			
		||||
                        ) {
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            items.remove(i)
 | 
			
		||||
                            thread {
 | 
			
		||||
                                db.itemsDao().delete(i.toEntity())
 | 
			
		||||
                            }
 | 
			
		||||
                            notifyItemRemoved(position)
 | 
			
		||||
                            updateItems(items)
 | 
			
		||||
                            doUnmark(i, position)
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                } else {
 | 
			
		||||
                    thread {
 | 
			
		||||
                        db.actionsDao().deleteReadActionForArticle(i.id)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun removeItemAtIndex(position: Int) {
 | 
			
		||||
        val i = items[position]
 | 
			
		||||
        items.remove(i)
 | 
			
		||||
        notifyItemRemoved(position)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
 | 
			
		||||
        thread {
 | 
			
		||||
            db.itemsDao().delete(i.toEntity())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (app.isNetworkAccessible(null)) {
 | 
			
		||||
            api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<SuccessResponse>,
 | 
			
		||||
                    response: Response<SuccessResponse>
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (!response.succeeded() && debugReadingItems) {
 | 
			
		||||
                        val message =
 | 
			
		||||
                            "message: ${response.message()} " +
 | 
			
		||||
                                    "response isSuccess: ${response.isSuccessful} " +
 | 
			
		||||
                                    "response code: ${response.code()} " +
 | 
			
		||||
                                    "response message: ${response.message()} " +
 | 
			
		||||
                                    "response errorBody: ${response.errorBody()?.string()} " +
 | 
			
		||||
                                    "body success: ${response.body()?.success} " +
 | 
			
		||||
                                    "body isSuccess: ${response.body()?.isSuccess}"
 | 
			
		||||
                        ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), app)
 | 
			
		||||
                        Toast.makeText(app.baseContext, message, Toast.LENGTH_LONG).show()
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    doUnmark(i, position)
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                    if (debugReadingItems) {
 | 
			
		||||
                        ACRA.getErrorReporter().maybeHandleSilentException(t, app)
 | 
			
		||||
                        Toast.makeText(app.baseContext, t.message, Toast.LENGTH_LONG).show()
 | 
			
		||||
                    }
 | 
			
		||||
                    Toast.makeText(
 | 
			
		||||
                        app,
 | 
			
		||||
                        app.getString(R.string.cant_mark_read),
 | 
			
		||||
                        Toast.LENGTH_SHORT
 | 
			
		||||
                    ).show()
 | 
			
		||||
                    items.add(i)
 | 
			
		||||
                    notifyItemInserted(position)
 | 
			
		||||
                    updateItems(items)
 | 
			
		||||
 | 
			
		||||
                    thread {
 | 
			
		||||
                        db.itemsDao().insertAllItems(i.toEntity())
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            thread {
 | 
			
		||||
                db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
 | 
			
		||||
                doUnmark(i, position)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemAtIndex(item: Item, position: Int) {
 | 
			
		||||
        items.add(position, item)
 | 
			
		||||
        notifyItemInserted(position)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun addItemsAtEnd(newItems: List<Item>) {
 | 
			
		||||
        val oldSize = items.size
 | 
			
		||||
        items.addAll(newItems)
 | 
			
		||||
        notifyItemRangeInserted(oldSize, newItems.size)
 | 
			
		||||
        updateItems(items)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,104 +2,105 @@ package apps.amine.bou.readerforselfoss.adapters
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import android.support.constraint.ConstraintLayout
 | 
			
		||||
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.Button
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Sources
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
 | 
			
		||||
import com.amulyakhare.textdrawable.TextDrawable
 | 
			
		||||
import com.amulyakhare.textdrawable.util.ColorGenerator
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
 | 
			
		||||
import kotlinx.android.synthetic.main.source_list_item.view.*
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
class SourcesListAdapter(private val app: Activity, private val items: ArrayList<Sources>, private val api: SelfossApi) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
 | 
			
		||||
class SourcesListAdapter(
 | 
			
		||||
    private val app: Activity,
 | 
			
		||||
    private val items: ArrayList<Source>,
 | 
			
		||||
    private val api: SelfossApi
 | 
			
		||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
 | 
			
		||||
    private val c: Context = app.baseContext
 | 
			
		||||
    private val generator: ColorGenerator = ColorGenerator.MATERIAL
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(R.layout.source_list_item, parent, false) as ConstraintLayout
 | 
			
		||||
        val v = LayoutInflater.from(c).inflate(
 | 
			
		||||
            R.layout.source_list_item,
 | 
			
		||||
            parent,
 | 
			
		||||
            false
 | 
			
		||||
        ) as ConstraintLayout
 | 
			
		||||
        return ViewHolder(v)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
 | 
			
		||||
        val itm = items[position]
 | 
			
		||||
 | 
			
		||||
        val fHolder = holder
 | 
			
		||||
        if (itm.getIcon(c).isEmpty()) {
 | 
			
		||||
            val color = generator.getColor(itm.title)
 | 
			
		||||
            val textDrawable = StringBuilder()
 | 
			
		||||
            for (s in itm.title.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
 | 
			
		||||
                textDrawable.append(s[0])
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            val builder = TextDrawable.builder().round()
 | 
			
		||||
 | 
			
		||||
            val drawable = builder.build(textDrawable.toString(), color)
 | 
			
		||||
            holder.sourceImage!!.setImageDrawable(drawable)
 | 
			
		||||
            val drawable =
 | 
			
		||||
                TextDrawable
 | 
			
		||||
                    .builder()
 | 
			
		||||
                    .round()
 | 
			
		||||
                    .build(itm.title.toTextDrawableString(c), color)
 | 
			
		||||
            holder.mView.itemImage.setImageDrawable(drawable)
 | 
			
		||||
        } else {
 | 
			
		||||
            Glide.with(c).load(itm.getIcon(c)).asBitmap().centerCrop().into(object : BitmapImageViewTarget(holder.sourceImage) {
 | 
			
		||||
                override fun setResource(resource: Bitmap) {
 | 
			
		||||
                    val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(c.resources, resource)
 | 
			
		||||
                    circularBitmapDrawable.isCircular = true
 | 
			
		||||
                    fHolder.sourceImage!!.setImageDrawable(circularBitmapDrawable)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            c.circularBitmapDrawable(itm.getIcon(c), holder.mView.itemImage)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.sourceTitle!!.text = itm.title
 | 
			
		||||
        holder.mView.sourceTitle.text = itm.title
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemCount(): Int {
 | 
			
		||||
        return items.size
 | 
			
		||||
    }
 | 
			
		||||
    override fun getItemCount(): Int = items.size
 | 
			
		||||
 | 
			
		||||
    inner class ViewHolder(internal val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
 | 
			
		||||
        var sourceImage: ImageView? = null
 | 
			
		||||
        var sourceTitle: TextView? = null
 | 
			
		||||
 | 
			
		||||
        init {
 | 
			
		||||
 | 
			
		||||
            handleClickListeners()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private fun handleClickListeners() {
 | 
			
		||||
            sourceImage = mView.findViewById(R.id.itemImage) as ImageView
 | 
			
		||||
            sourceTitle = mView.findViewById(R.id.sourceTitle) as TextView
 | 
			
		||||
 | 
			
		||||
            val deleteBtn = mView.findViewById(R.id.deleteBtn) as Button
 | 
			
		||||
            val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
 | 
			
		||||
 | 
			
		||||
            deleteBtn.setOnClickListener {
 | 
			
		||||
                val (id) = items[adapterPosition]
 | 
			
		||||
                api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                    override fun onResponse(call: Call<SuccessResponse>, response: Response<SuccessResponse>) {
 | 
			
		||||
                        if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                            items.removeAt(adapterPosition)
 | 
			
		||||
                            notifyItemRemoved(adapterPosition)
 | 
			
		||||
                            notifyItemRangeChanged(adapterPosition, itemCount)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
 | 
			
		||||
                if (c.isNetworkAccessible(null)) {
 | 
			
		||||
                    val (id) = items[adapterPosition]
 | 
			
		||||
                    api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
 | 
			
		||||
                        override fun onResponse(
 | 
			
		||||
                            call: Call<SuccessResponse>,
 | 
			
		||||
                            response: Response<SuccessResponse>
 | 
			
		||||
                        ) {
 | 
			
		||||
                            if (response.body() != null && response.body()!!.isSuccess) {
 | 
			
		||||
                                items.removeAt(adapterPosition)
 | 
			
		||||
                                notifyItemRemoved(adapterPosition)
 | 
			
		||||
                                notifyItemRangeChanged(adapterPosition, itemCount)
 | 
			
		||||
                            } else {
 | 
			
		||||
                                Toast.makeText(
 | 
			
		||||
                                    app,
 | 
			
		||||
                                    R.string.can_delete_source,
 | 
			
		||||
                                    Toast.LENGTH_SHORT
 | 
			
		||||
                                ).show()
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                        Toast.makeText(app, "Petit soucis lors de la suppression de la source.", Toast.LENGTH_SHORT).show()
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                        override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
 | 
			
		||||
                            Toast.makeText(
 | 
			
		||||
                                app,
 | 
			
		||||
                                R.string.can_delete_source,
 | 
			
		||||
                                Toast.LENGTH_SHORT
 | 
			
		||||
                            ).show()
 | 
			
		||||
                        }
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.mercury
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import com.google.gson.GsonBuilder
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.logging.HttpLoggingInterceptor
 | 
			
		||||
@@ -8,25 +7,33 @@ import retrofit2.Call
 | 
			
		||||
import retrofit2.Retrofit
 | 
			
		||||
import retrofit2.converter.gson.GsonConverterFactory
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MercuryApi(private val key: String) {
 | 
			
		||||
class MercuryApi(shouldLog: Boolean) {
 | 
			
		||||
    private val service: MercuryService
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
 | 
			
		||||
        val interceptor = HttpLoggingInterceptor()
 | 
			
		||||
        interceptor.level = HttpLoggingInterceptor.Level.BODY
 | 
			
		||||
        interceptor.level = if (shouldLog) {
 | 
			
		||||
            HttpLoggingInterceptor.Level.BODY
 | 
			
		||||
        } else {
 | 
			
		||||
            HttpLoggingInterceptor.Level.NONE
 | 
			
		||||
        }
 | 
			
		||||
        val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
 | 
			
		||||
 | 
			
		||||
        val gson = GsonBuilder()
 | 
			
		||||
                .setLenient()
 | 
			
		||||
                .create()
 | 
			
		||||
        val retrofit = Retrofit.Builder().baseUrl("https://mercury.postlight.com").client(client)
 | 
			
		||||
                .addConverterFactory(GsonConverterFactory.create(gson)).build()
 | 
			
		||||
            .setLenient()
 | 
			
		||||
            .create()
 | 
			
		||||
        val retrofit =
 | 
			
		||||
            Retrofit
 | 
			
		||||
                .Builder()
 | 
			
		||||
                .baseUrl("https://www.amine-bou.fr")
 | 
			
		||||
                .client(client)
 | 
			
		||||
                .addConverterFactory(GsonConverterFactory.create(gson))
 | 
			
		||||
                .build()
 | 
			
		||||
        service = retrofit.create(MercuryService::class.java)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun parseUrl(url: String): Call<ParsedContent> {
 | 
			
		||||
        return service.parseUrl(url, this.key)
 | 
			
		||||
        return service.parseUrl(url)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,39 +2,43 @@ package apps.amine.bou.readerforselfoss.api.mercury
 | 
			
		||||
 | 
			
		||||
import android.os.Parcel
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
import com.google.gson.annotations.SerializedName
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ParsedContent(val title: String,
 | 
			
		||||
                    val content: String,
 | 
			
		||||
                    val date_published: String,
 | 
			
		||||
                    val lead_image_url: String,
 | 
			
		||||
                    val dek: String,
 | 
			
		||||
                    val url: String,
 | 
			
		||||
                    val domain: String,
 | 
			
		||||
                    val excerpt: String,
 | 
			
		||||
                    val total_pages: Int,
 | 
			
		||||
                    val rendered_pages: Int,
 | 
			
		||||
                    val next_page_url: String) : Parcelable {
 | 
			
		||||
class ParsedContent(
 | 
			
		||||
    @SerializedName("title") val title: String,
 | 
			
		||||
    @SerializedName("content") val content: String?,
 | 
			
		||||
    @SerializedName("date_published") val date_published: String,
 | 
			
		||||
    @SerializedName("lead_image_url") val lead_image_url: String?,
 | 
			
		||||
    @SerializedName("dek") val dek: String,
 | 
			
		||||
    @SerializedName("url") val url: String,
 | 
			
		||||
    @SerializedName("domain") val domain: String,
 | 
			
		||||
    @SerializedName("excerpt") val excerpt: String,
 | 
			
		||||
    @SerializedName("total_pages") val total_pages: Int,
 | 
			
		||||
    @SerializedName("rendered_pages") val rendered_pages: Int,
 | 
			
		||||
    @SerializedName("next_page_url") val next_page_url: String
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        @JvmField val CREATOR: Parcelable.Creator<ParsedContent> = object : Parcelable.Creator<ParsedContent> {
 | 
			
		||||
            override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
 | 
			
		||||
            override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
 | 
			
		||||
        }
 | 
			
		||||
        @JvmField
 | 
			
		||||
        val CREATOR: Parcelable.Creator<ParsedContent> =
 | 
			
		||||
            object : Parcelable.Creator<ParsedContent> {
 | 
			
		||||
                override fun createFromParcel(source: Parcel): ParsedContent = ParsedContent(source)
 | 
			
		||||
                override fun newArray(size: Int): Array<ParsedContent?> = arrayOfNulls(size)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(source: Parcel) : this(
 | 
			
		||||
            title = source.readString(),
 | 
			
		||||
            content = source.readString(),
 | 
			
		||||
            date_published = source.readString(),
 | 
			
		||||
            lead_image_url = source.readString(),
 | 
			
		||||
            dek = source.readString(),
 | 
			
		||||
            url = source.readString(),
 | 
			
		||||
            domain = source.readString(),
 | 
			
		||||
            excerpt = source.readString(),
 | 
			
		||||
            total_pages = source.readInt(),
 | 
			
		||||
            rendered_pages = source.readInt(),
 | 
			
		||||
            next_page_url = source.readString()
 | 
			
		||||
        title = source.readString(),
 | 
			
		||||
        content = source.readString(),
 | 
			
		||||
        date_published = source.readString(),
 | 
			
		||||
        lead_image_url = source.readString(),
 | 
			
		||||
        dek = source.readString(),
 | 
			
		||||
        url = source.readString(),
 | 
			
		||||
        domain = source.readString(),
 | 
			
		||||
        excerpt = source.readString(),
 | 
			
		||||
        total_pages = source.readInt(),
 | 
			
		||||
        rendered_pages = source.readInt(),
 | 
			
		||||
        next_page_url = source.readString()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun describeContents() = 0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.mercury
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.http.GET
 | 
			
		||||
import retrofit2.http.Header
 | 
			
		||||
import retrofit2.http.Query
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface MercuryService {
 | 
			
		||||
    @GET("parser")
 | 
			
		||||
    fun parseUrl(@Query("url") url: String, @Header("x-api-key") key: String): Call<ParsedContent>
 | 
			
		||||
    @GET("parser.php")
 | 
			
		||||
    fun parseUrl(@Query("link") link: String): Call<ParsedContent>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,19 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonParseException
 | 
			
		||||
import com.google.gson.JsonDeserializationContext
 | 
			
		||||
import com.google.gson.JsonElement
 | 
			
		||||
import com.google.gson.JsonDeserializer
 | 
			
		||||
import com.google.gson.JsonElement
 | 
			
		||||
import com.google.gson.JsonParseException
 | 
			
		||||
import java.lang.reflect.Type
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
 | 
			
		||||
 | 
			
		||||
    @Throws(JsonParseException::class)
 | 
			
		||||
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean? =
 | 
			
		||||
    override fun deserialize(
 | 
			
		||||
        json: JsonElement,
 | 
			
		||||
        typeOfT: Type,
 | 
			
		||||
        context: JsonDeserializationContext
 | 
			
		||||
    ): Boolean? =
 | 
			
		||||
        try {
 | 
			
		||||
            json.asInt == 1
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
 | 
			
		||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
 | 
			
		||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
 | 
			
		||||
import com.burgstaller.okhttp.DispatchingAuthenticator
 | 
			
		||||
@@ -17,94 +18,154 @@ import retrofit2.Call
 | 
			
		||||
import retrofit2.Retrofit
 | 
			
		||||
import retrofit2.converter.gson.GsonConverterFactory
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap
 | 
			
		||||
import java.util.concurrent.TimeUnit
 | 
			
		||||
 | 
			
		||||
class SelfossApi(
 | 
			
		||||
    c: Context,
 | 
			
		||||
    callingActivity: Activity?,
 | 
			
		||||
    isWithSelfSignedCert: Boolean,
 | 
			
		||||
    timeout: Long,
 | 
			
		||||
    shouldLog: Boolean
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
class SelfossApi(c: Context) {
 | 
			
		||||
 | 
			
		||||
    private val service: SelfossService
 | 
			
		||||
    private lateinit var service: SelfossService
 | 
			
		||||
    private val config: Config = Config(c)
 | 
			
		||||
    private val userName: String
 | 
			
		||||
    private val password: String
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
    fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder =
 | 
			
		||||
        if (isWithSelfSignedCert) {
 | 
			
		||||
            getUnsafeHttpClient()
 | 
			
		||||
        } else {
 | 
			
		||||
            this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val interceptor = HttpLoggingInterceptor()
 | 
			
		||||
        interceptor.level = HttpLoggingInterceptor.Level.BODY
 | 
			
		||||
    fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
 | 
			
		||||
        if (timeout != -1L) {
 | 
			
		||||
            this.readTimeout(timeout, TimeUnit.SECONDS)
 | 
			
		||||
                .connectTimeout(timeout, TimeUnit.SECONDS)
 | 
			
		||||
        } else {
 | 
			
		||||
            this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val httpBuilder = OkHttpClient.Builder()
 | 
			
		||||
    fun Credentials.createAuthenticator(): DispatchingAuthenticator =
 | 
			
		||||
        DispatchingAuthenticator.Builder()
 | 
			
		||||
            .with("digest", DigestAuthenticator(this))
 | 
			
		||||
            .with("basic", BasicAuthenticator(this))
 | 
			
		||||
            .build()
 | 
			
		||||
 | 
			
		||||
    fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
 | 
			
		||||
        val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
 | 
			
		||||
        return OkHttpClient
 | 
			
		||||
            .Builder()
 | 
			
		||||
            .maybeWithSettingsTimeout(timeout)
 | 
			
		||||
            .maybeWithSelfSigned(isWithSelfSignedCert)
 | 
			
		||||
            .authenticator(CachingAuthenticatorDecorator(this, authCache))
 | 
			
		||||
            .addInterceptor(AuthenticationCacheInterceptor(authCache))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        val httpUserName = config.httpUserLogin
 | 
			
		||||
        val httpPassword = config.httpUserPassword
 | 
			
		||||
    init {
 | 
			
		||||
        userName = config.userLogin
 | 
			
		||||
        password = config.userPassword
 | 
			
		||||
 | 
			
		||||
        val credentials = Credentials(httpUserName, httpPassword)
 | 
			
		||||
        val basicAuthenticator = BasicAuthenticator(credentials)
 | 
			
		||||
        val digestAuthenticator = DigestAuthenticator(credentials)
 | 
			
		||||
        val authenticator =
 | 
			
		||||
            Credentials(
 | 
			
		||||
                config.httpUserLogin,
 | 
			
		||||
                config.httpUserPassword
 | 
			
		||||
            ).createAuthenticator()
 | 
			
		||||
 | 
			
		||||
        // note that all auth schemes should be registered as lowercase!
 | 
			
		||||
        val authenticator = DispatchingAuthenticator.Builder()
 | 
			
		||||
                .with("digest", digestAuthenticator)
 | 
			
		||||
                .with("basic", basicAuthenticator)
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
        val client = httpBuilder
 | 
			
		||||
                .authenticator(CachingAuthenticatorDecorator(authenticator, authCache))
 | 
			
		||||
                .addInterceptor(AuthenticationCacheInterceptor(authCache))
 | 
			
		||||
                .addInterceptor(interceptor)
 | 
			
		||||
                .build()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        val builder = GsonBuilder()
 | 
			
		||||
        builder.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
 | 
			
		||||
 | 
			
		||||
        val gson = builder
 | 
			
		||||
        val gson =
 | 
			
		||||
            GsonBuilder()
 | 
			
		||||
                .registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
 | 
			
		||||
                .registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
 | 
			
		||||
                .setLenient()
 | 
			
		||||
                .create()
 | 
			
		||||
 | 
			
		||||
        userName = config.userLogin
 | 
			
		||||
        password = config.userPassword
 | 
			
		||||
        val retrofit = Retrofit.Builder().baseUrl(config.baseUrl).client(client)
 | 
			
		||||
                .addConverterFactory(GsonConverterFactory.create(gson)).build()
 | 
			
		||||
        service = retrofit.create(SelfossService::class.java)
 | 
			
		||||
        val logging = HttpLoggingInterceptor()
 | 
			
		||||
 | 
			
		||||
        logging.level = if (shouldLog) {
 | 
			
		||||
            HttpLoggingInterceptor.Level.BODY
 | 
			
		||||
        } else {
 | 
			
		||||
            HttpLoggingInterceptor.Level.NONE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
 | 
			
		||||
 | 
			
		||||
        httpClient.addInterceptor(logging)
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val retrofit =
 | 
			
		||||
                Retrofit
 | 
			
		||||
                    .Builder()
 | 
			
		||||
                    .baseUrl(config.baseUrl)
 | 
			
		||||
                    .client(httpClient.build())
 | 
			
		||||
                    .addConverterFactory(GsonConverterFactory.create(gson))
 | 
			
		||||
                    .build()
 | 
			
		||||
            service = retrofit.create(SelfossService::class.java)
 | 
			
		||||
        } catch (e: IllegalArgumentException) {
 | 
			
		||||
            if (callingActivity != null) {
 | 
			
		||||
                Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun login(): Call<SuccessResponse> {
 | 
			
		||||
        return service.loginToSelfoss(config.userLogin, config.userPassword)
 | 
			
		||||
    }
 | 
			
		||||
    fun login(): Call<SuccessResponse> =
 | 
			
		||||
        service.loginToSelfoss(config.userLogin, config.userPassword)
 | 
			
		||||
 | 
			
		||||
    fun readItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
 | 
			
		||||
        getItems("read", tag, sourceId, search)
 | 
			
		||||
    fun readItems(
 | 
			
		||||
        tag: String?,
 | 
			
		||||
        sourceId: Long?,
 | 
			
		||||
        search: String?,
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): Call<List<Item>> =
 | 
			
		||||
        getItems("read", tag, sourceId, search, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    fun unreadItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
 | 
			
		||||
        getItems("unread", tag, sourceId, search)
 | 
			
		||||
    fun newItems(
 | 
			
		||||
        tag: String?,
 | 
			
		||||
        sourceId: Long?,
 | 
			
		||||
        search: String?,
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): Call<List<Item>> =
 | 
			
		||||
        getItems("unread", tag, sourceId, search, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    fun starredItems(tag: String?, sourceId: Long?, search: String?): Call<List<Item>> =
 | 
			
		||||
        getItems("starred", tag, sourceId, search)
 | 
			
		||||
    fun starredItems(
 | 
			
		||||
        tag: String?,
 | 
			
		||||
        sourceId: Long?,
 | 
			
		||||
        search: String?,
 | 
			
		||||
        itemsNumber: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): Call<List<Item>> =
 | 
			
		||||
        getItems("starred", tag, sourceId, search, itemsNumber, offset)
 | 
			
		||||
 | 
			
		||||
    private fun getItems(type: String, tag: String?, sourceId: Long?, search: String?): Call<List<Item>> {
 | 
			
		||||
        return service.getItems(type, tag, sourceId, search, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun allItems(): Call<List<Item>> =
 | 
			
		||||
        service.allItems(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun markItem(itemId: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.markAsRead(itemId, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    private fun getItems(
 | 
			
		||||
        type: String,
 | 
			
		||||
        tag: String?,
 | 
			
		||||
        sourceId: Long?,
 | 
			
		||||
        search: String?,
 | 
			
		||||
        items: Int,
 | 
			
		||||
        offset: Int
 | 
			
		||||
    ): Call<List<Item>> =
 | 
			
		||||
        service.getItems(type, tag, sourceId, search, userName, password, items, offset)
 | 
			
		||||
 | 
			
		||||
    fun unmarkItem(itemId: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.unmarkAsRead(itemId, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun markItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.markAsRead(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun readAll(ids: List<String>): Call<SuccessResponse> {
 | 
			
		||||
        return service.markAllAsRead(ids, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun unmarkItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.unmarkAsRead(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun starrItem(itemId: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.starr(itemId, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun readAll(ids: List<String>): Call<SuccessResponse> =
 | 
			
		||||
        service.markAllAsRead(ids, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun starrItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.starr(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun unstarrItem(itemId: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.unstarr(itemId, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun unstarrItem(itemId: String): Call<SuccessResponse> =
 | 
			
		||||
        service.unstarr(itemId, userName, password)
 | 
			
		||||
 | 
			
		||||
    val stats: Call<Stats>
 | 
			
		||||
        get() = service.stats(userName, password)
 | 
			
		||||
@@ -112,23 +173,24 @@ class SelfossApi(c: Context) {
 | 
			
		||||
    val tags: Call<List<Tag>>
 | 
			
		||||
        get() = service.tags(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun update(): Call<String> {
 | 
			
		||||
        return service.update(userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun update(): Call<String> =
 | 
			
		||||
        service.update(userName, password)
 | 
			
		||||
 | 
			
		||||
    val sources: Call<List<Sources>>
 | 
			
		||||
    val sources: Call<List<Source>>
 | 
			
		||||
        get() = service.sources(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun deleteSource(id: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.deleteSource(id, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun deleteSource(id: String): Call<SuccessResponse> =
 | 
			
		||||
        service.deleteSource(id, userName, password)
 | 
			
		||||
 | 
			
		||||
    fun spouts(): Call<Map<String, Spout>> {
 | 
			
		||||
        return service.spouts(userName, password)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun createSource(title: String, url: String, spout: String, tags: String, filter: String): Call<SuccessResponse> {
 | 
			
		||||
        return service.createSource(title, url, spout, tags, filter, userName, password)
 | 
			
		||||
    }
 | 
			
		||||
    fun spouts(): Call<Map<String, Spout>> =
 | 
			
		||||
        service.spouts(userName, password)
 | 
			
		||||
 | 
			
		||||
    fun createSource(
 | 
			
		||||
        title: String,
 | 
			
		||||
        url: String,
 | 
			
		||||
        spout: String,
 | 
			
		||||
        tags: String,
 | 
			
		||||
        filter: String
 | 
			
		||||
    ): Call<SuccessResponse> =
 | 
			
		||||
        service.createSource(title, url, spout, tags, filter, userName, password)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,56 +4,75 @@ import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.os.Parcel
 | 
			
		||||
import android.os.Parcelable
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
 | 
			
		||||
import com.google.gson.annotations.SerializedName
 | 
			
		||||
 | 
			
		||||
private fun constructUrl(config: Config?, path: String, file: String?): String {
 | 
			
		||||
    return if (file.isEmptyOrNullOrNullString()) {
 | 
			
		||||
        ""
 | 
			
		||||
    } else {
 | 
			
		||||
        val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
 | 
			
		||||
        baseUriBuilder.appendPath(path).appendPath(file)
 | 
			
		||||
 | 
			
		||||
private fun constructUrl(config: Config?, path: String, file: String): String {
 | 
			
		||||
    val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
 | 
			
		||||
    baseUriBuilder.appendPath(path).appendPath(file)
 | 
			
		||||
 | 
			
		||||
    return if (isEmptyOrNullOrNullString(file)) ""
 | 
			
		||||
    else baseUriBuilder.toString()
 | 
			
		||||
        baseUriBuilder.toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class Tag(
 | 
			
		||||
    @SerializedName("tag") val tag: String,
 | 
			
		||||
    @SerializedName("color") val color: String,
 | 
			
		||||
    @SerializedName("unread") val unread: Int
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class Tag(val tag: String, val color: String, val unread: Int)
 | 
			
		||||
 | 
			
		||||
class SuccessResponse(val success: Boolean) {
 | 
			
		||||
class SuccessResponse(@SerializedName("success") val success: Boolean) {
 | 
			
		||||
    val isSuccess: Boolean
 | 
			
		||||
        get() = success
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Stats(val total: Int, val unread: Int, val starred: Int)
 | 
			
		||||
class Stats(
 | 
			
		||||
    @SerializedName("total") val total: Int,
 | 
			
		||||
    @SerializedName("unread") val unread: Int,
 | 
			
		||||
    @SerializedName("starred") val starred: Int
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class Spout(val name: String, val description: String)
 | 
			
		||||
data class Spout(
 | 
			
		||||
    @SerializedName("name") val name: String,
 | 
			
		||||
    @SerializedName("description") val description: String
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
data class Sources(val id: String,
 | 
			
		||||
                   val title: String,
 | 
			
		||||
                   val tags: String,
 | 
			
		||||
                   val spout: String,
 | 
			
		||||
                   val error: String,
 | 
			
		||||
                   val icon: String) {
 | 
			
		||||
data class Source(
 | 
			
		||||
    @SerializedName("id") val id: String,
 | 
			
		||||
    @SerializedName("title") val title: String,
 | 
			
		||||
    @SerializedName("tags") val tags: SelfossTagType,
 | 
			
		||||
    @SerializedName("spout") val spout: String,
 | 
			
		||||
    @SerializedName("error") val error: String,
 | 
			
		||||
    @SerializedName("icon") val icon: String
 | 
			
		||||
) {
 | 
			
		||||
    var config: Config? = null
 | 
			
		||||
 | 
			
		||||
    fun getIcon(app: Context): String {
 | 
			
		||||
        if (config == null) {
 | 
			
		||||
            config = Config(app)
 | 
			
		||||
        }
 | 
			
		||||
        return constructUrl(config,"favicons", icon)
 | 
			
		||||
        return constructUrl(config, "favicons", icon)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class Item(val id: String,
 | 
			
		||||
                val datetime: String,
 | 
			
		||||
                val title: String,
 | 
			
		||||
                val unread: Boolean,
 | 
			
		||||
                val starred: Boolean,
 | 
			
		||||
                val thumbnail: String,
 | 
			
		||||
                val icon: String,
 | 
			
		||||
                val link: String,
 | 
			
		||||
                val sourcetitle: String) : Parcelable {
 | 
			
		||||
data class Item(
 | 
			
		||||
    @SerializedName("id") val id: String,
 | 
			
		||||
    @SerializedName("datetime") val datetime: String,
 | 
			
		||||
    @SerializedName("title") val title: String,
 | 
			
		||||
    @SerializedName("content") val content: String,
 | 
			
		||||
    @SerializedName("unread") val unread: Boolean,
 | 
			
		||||
    @SerializedName("starred") var starred: Boolean,
 | 
			
		||||
    @SerializedName("thumbnail") val thumbnail: String,
 | 
			
		||||
    @SerializedName("icon") val icon: String,
 | 
			
		||||
    @SerializedName("link") val link: String,
 | 
			
		||||
    @SerializedName("sourcetitle") val sourcetitle: String,
 | 
			
		||||
    @SerializedName("tags") val tags: SelfossTagType
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
 | 
			
		||||
    var config: Config? = null
 | 
			
		||||
 | 
			
		||||
@@ -65,15 +84,17 @@ data class Item(val id: String,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(source: Parcel) : this(
 | 
			
		||||
            id = source.readString(),
 | 
			
		||||
            datetime = source.readString(),
 | 
			
		||||
            title = source.readString(),
 | 
			
		||||
            unread = 0.toByte() != source.readByte(),
 | 
			
		||||
            starred = 0.toByte() != source.readByte(),
 | 
			
		||||
            thumbnail = source.readString(),
 | 
			
		||||
            icon = source.readString(),
 | 
			
		||||
            link = source.readString(),
 | 
			
		||||
            sourcetitle = source.readString()
 | 
			
		||||
        id = source.readString(),
 | 
			
		||||
        datetime = source.readString(),
 | 
			
		||||
        title = source.readString(),
 | 
			
		||||
        content = source.readString(),
 | 
			
		||||
        unread = 0.toByte() != source.readByte(),
 | 
			
		||||
        starred = 0.toByte() != source.readByte(),
 | 
			
		||||
        thumbnail = source.readString(),
 | 
			
		||||
        icon = source.readString(),
 | 
			
		||||
        link = source.readString(),
 | 
			
		||||
        sourcetitle = source.readString(),
 | 
			
		||||
        tags = source.readParcelable(ClassLoader.getSystemClassLoader())
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun describeContents() = 0
 | 
			
		||||
@@ -82,12 +103,14 @@ data class Item(val id: String,
 | 
			
		||||
        dest.writeString(id)
 | 
			
		||||
        dest.writeString(datetime)
 | 
			
		||||
        dest.writeString(title)
 | 
			
		||||
        dest.writeString(content)
 | 
			
		||||
        dest.writeByte((if (unread) 1 else 0))
 | 
			
		||||
        dest.writeByte((if (starred) 1 else 0))
 | 
			
		||||
        dest.writeString(thumbnail)
 | 
			
		||||
        dest.writeString(icon)
 | 
			
		||||
        dest.writeString(link)
 | 
			
		||||
        dest.writeString(sourcetitle)
 | 
			
		||||
        dest.writeParcelable(tags, flags)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun getIcon(app: Context): String {
 | 
			
		||||
@@ -107,21 +130,50 @@ data class Item(val id: String,
 | 
			
		||||
    // TODO: maybe find a better way to handle these kind of urls
 | 
			
		||||
    fun getLinkDecoded(): String {
 | 
			
		||||
        var stringUrl: String
 | 
			
		||||
        if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
 | 
			
		||||
            if (link.contains("&url=")) {
 | 
			
		||||
                stringUrl = link.substringAfter("&url=")
 | 
			
		||||
            } else {
 | 
			
		||||
                stringUrl = this.link.replace("&", "&")
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            stringUrl = this.link.replace("&", "&")
 | 
			
		||||
        }
 | 
			
		||||
        stringUrl =
 | 
			
		||||
                if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
 | 
			
		||||
                    if (link.contains("&url=")) {
 | 
			
		||||
                        link.substringAfter("&url=")
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.link.replace("&", "&")
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.link.replace("&", "&")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
        // handle :443 => https
 | 
			
		||||
        if (stringUrl.contains(":443")) {
 | 
			
		||||
            stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // handle url not starting with http
 | 
			
		||||
        if (stringUrl.startsWith("//")) {
 | 
			
		||||
            stringUrl = "http:$stringUrl"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return stringUrl
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
data class SelfossTagType(val tags: String) : Parcelable {
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        @JvmField val CREATOR: Parcelable.Creator<SelfossTagType> =
 | 
			
		||||
            object : Parcelable.Creator<SelfossTagType> {
 | 
			
		||||
                override fun createFromParcel(source: Parcel): SelfossTagType =
 | 
			
		||||
                    SelfossTagType(source)
 | 
			
		||||
 | 
			
		||||
                override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(source: Parcel) : this(
 | 
			
		||||
        tags = source.readString()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun describeContents() = 0
 | 
			
		||||
 | 
			
		||||
    override fun writeToParcel(dest: Parcel, flags: Int) {
 | 
			
		||||
        dest.writeString(tags)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,70 +1,124 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.http.DELETE
 | 
			
		||||
import retrofit2.http.Field
 | 
			
		||||
import retrofit2.http.FormUrlEncoded
 | 
			
		||||
import retrofit2.http.GET
 | 
			
		||||
import retrofit2.http.Headers
 | 
			
		||||
import retrofit2.http.POST
 | 
			
		||||
import retrofit2.http.Path
 | 
			
		||||
import retrofit2.http.Query
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
internal interface SelfossService {
 | 
			
		||||
 | 
			
		||||
    @GET("login")
 | 
			
		||||
    fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @GET("items")
 | 
			
		||||
    fun getItems(@Query("type") type: String,
 | 
			
		||||
                 @Query("tag") tag: String?,
 | 
			
		||||
                 @Query("source") source: Long?,
 | 
			
		||||
                 @Query("search") search: String?,
 | 
			
		||||
                 @Query("username") username: String,
 | 
			
		||||
                 @Query("password") password: String): Call<List<Item>>
 | 
			
		||||
    fun getItems(
 | 
			
		||||
        @Query("type") type: String,
 | 
			
		||||
        @Query("tag") tag: String?,
 | 
			
		||||
        @Query("source") source: Long?,
 | 
			
		||||
        @Query("search") search: String?,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String,
 | 
			
		||||
        @Query("items") items: Int,
 | 
			
		||||
        @Query("offset") offset: Int
 | 
			
		||||
    ): Call<List<Item>>
 | 
			
		||||
 | 
			
		||||
    @GET("items")
 | 
			
		||||
    fun allItems(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Item>>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("mark/{id}")
 | 
			
		||||
    fun markAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    fun markAsRead(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("unmark/{id}")
 | 
			
		||||
    fun unmarkAsRead(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
    fun unmarkAsRead(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @FormUrlEncoded
 | 
			
		||||
    @POST("mark")
 | 
			
		||||
    fun markAllAsRead(@Field("ids[]") ids: List<String>, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    fun markAllAsRead(
 | 
			
		||||
        @Field("ids[]") ids: List<String>,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("starr/{id}")
 | 
			
		||||
    fun starr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    fun starr(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @Headers("Content-Type: application/x-www-form-urlencoded")
 | 
			
		||||
    @POST("unstarr/{id}")
 | 
			
		||||
    fun unstarr(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    fun unstarr(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @GET("stats")
 | 
			
		||||
    fun stats(@Query("username") username: String, @Query("password") password: String): Call<Stats>
 | 
			
		||||
 | 
			
		||||
    fun stats(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<Stats>
 | 
			
		||||
 | 
			
		||||
    @GET("tags")
 | 
			
		||||
    fun tags(@Query("username") username: String, @Query("password") password: String): Call<List<Tag>>
 | 
			
		||||
 | 
			
		||||
    fun tags(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Tag>>
 | 
			
		||||
 | 
			
		||||
    @GET("update")
 | 
			
		||||
    fun update(@Query("username") username: String, @Query("password") password: String): Call<String>
 | 
			
		||||
    fun update(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<String>
 | 
			
		||||
 | 
			
		||||
    @GET("sources/spouts")
 | 
			
		||||
    fun spouts(@Query("username") username: String, @Query("password") password: String): Call<Map<String, Spout>>
 | 
			
		||||
    fun spouts(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<Map<String, Spout>>
 | 
			
		||||
 | 
			
		||||
    @GET("sources/list")
 | 
			
		||||
    fun sources(@Query("username") username: String, @Query("password") password: String): Call<List<Sources>>
 | 
			
		||||
 | 
			
		||||
    fun sources(
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<List<Source>>
 | 
			
		||||
 | 
			
		||||
    @DELETE("source/{id}")
 | 
			
		||||
    fun deleteSource(@Path("id") id: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
    fun deleteSource(
 | 
			
		||||
        @Path("id") id: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
 | 
			
		||||
    @FormUrlEncoded
 | 
			
		||||
    @POST("source")
 | 
			
		||||
    fun createSource(@Field("title") title: String, @Field("url") url: String, @Field("spout") spout: String, @Field("tags") tags: String, @Field("filter") filter: String, @Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
 | 
			
		||||
    fun createSource(
 | 
			
		||||
        @Field("title") title: String,
 | 
			
		||||
        @Field("url") url: String,
 | 
			
		||||
        @Field("spout") spout: String,
 | 
			
		||||
        @Field("tags") tags: String,
 | 
			
		||||
        @Field("filter") filter: String,
 | 
			
		||||
        @Query("username") username: String,
 | 
			
		||||
        @Query("password") password: String
 | 
			
		||||
    ): Call<SuccessResponse>
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.api.selfoss
 | 
			
		||||
 | 
			
		||||
import com.google.gson.JsonDeserializationContext
 | 
			
		||||
import com.google.gson.JsonDeserializer
 | 
			
		||||
import com.google.gson.JsonElement
 | 
			
		||||
import com.google.gson.JsonParseException
 | 
			
		||||
import java.lang.reflect.Type
 | 
			
		||||
 | 
			
		||||
internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> {
 | 
			
		||||
 | 
			
		||||
    @Throws(JsonParseException::class)
 | 
			
		||||
    override fun deserialize(
 | 
			
		||||
        json: JsonElement,
 | 
			
		||||
        typeOfT: Type,
 | 
			
		||||
        context: JsonDeserializationContext
 | 
			
		||||
    ): SelfossTagType? =
 | 
			
		||||
        if (json.isJsonArray) {
 | 
			
		||||
            SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() })
 | 
			
		||||
        } else {
 | 
			
		||||
            SelfossTagType(json.toString())
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,152 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.background
 | 
			
		||||
 | 
			
		||||
import android.app.NotificationManager
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.core.app.NotificationCompat
 | 
			
		||||
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
 | 
			
		||||
import androidx.core.app.NotificationCompat.PRIORITY_LOW
 | 
			
		||||
import androidx.room.Room
 | 
			
		||||
import androidx.work.Worker
 | 
			
		||||
import androidx.work.WorkerParameters
 | 
			
		||||
import apps.amine.bou.readerforselfoss.MainActivity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.util.*
 | 
			
		||||
import kotlin.concurrent.schedule
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
 | 
			
		||||
    lateinit var db: AppDatabase
 | 
			
		||||
 | 
			
		||||
    override fun doWork(): Result {
 | 
			
		||||
        if (context.isNetworkAccessible(null)) {
 | 
			
		||||
 | 
			
		||||
            val notificationManager =
 | 
			
		||||
                applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 | 
			
		||||
 | 
			
		||||
            val notification = NotificationCompat.Builder(applicationContext, Config.syncChannelId)
 | 
			
		||||
                .setContentTitle(context.getString(R.string.loading_notification_title))
 | 
			
		||||
                .setContentText(context.getString(R.string.loading_notification_text))
 | 
			
		||||
                .setOngoing(true)
 | 
			
		||||
                .setPriority(PRIORITY_LOW)
 | 
			
		||||
                .setChannelId(Config.syncChannelId)
 | 
			
		||||
                .setSmallIcon(R.drawable.ic_cloud_download)
 | 
			
		||||
 | 
			
		||||
            notificationManager.notify(1, notification.build())
 | 
			
		||||
 | 
			
		||||
            val settings =
 | 
			
		||||
                this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
            val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
 | 
			
		||||
            val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
 | 
			
		||||
 | 
			
		||||
            db = Room.databaseBuilder(
 | 
			
		||||
                applicationContext,
 | 
			
		||||
                AppDatabase::class.java, "selfoss-database"
 | 
			
		||||
            ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
 | 
			
		||||
 | 
			
		||||
            val api = SelfossApi(
 | 
			
		||||
                this.context,
 | 
			
		||||
                null,
 | 
			
		||||
                settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
                sharedPref.getString("api_timeout", "-1").toLong(),
 | 
			
		||||
                sharedPref.getBoolean("should_log_everything", false)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            api.allItems().enqueue(object : Callback<List<Item>> {
 | 
			
		||||
                override fun onFailure(call: Call<List<Item>>, t: Throwable) {
 | 
			
		||||
                    Timer("", false).schedule(4000) {
 | 
			
		||||
                        notificationManager.cancel(1)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                override fun onResponse(
 | 
			
		||||
                    call: Call<List<Item>>,
 | 
			
		||||
                    response: Response<List<Item>>
 | 
			
		||||
                ) {
 | 
			
		||||
                    thread {
 | 
			
		||||
                        if (response.body() != null) {
 | 
			
		||||
                            val apiItems = (response.body() as ArrayList<Item>)
 | 
			
		||||
                            db.itemsDao().deleteAllItems()
 | 
			
		||||
                            db.itemsDao()
 | 
			
		||||
                                .insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
 | 
			
		||||
 | 
			
		||||
                            val newSize = apiItems.filter { it.unread }.size
 | 
			
		||||
                            if (notifyNewItems && newSize > 0) {
 | 
			
		||||
 | 
			
		||||
                                val intent = Intent(context, MainActivity::class.java).apply {
 | 
			
		||||
                                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
 | 
			
		||||
                                }
 | 
			
		||||
                                val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
 | 
			
		||||
 | 
			
		||||
                                val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
 | 
			
		||||
                                    .setContentTitle(context.getString(R.string.new_items_notification_title))
 | 
			
		||||
                                    .setContentText(context.getString(R.string.new_items_notification_text, newSize))
 | 
			
		||||
                                    .setPriority(PRIORITY_DEFAULT)
 | 
			
		||||
                                    .setChannelId(Config.newItemsChannelId)
 | 
			
		||||
                                    .setContentIntent(pendingIntent)
 | 
			
		||||
                                    .setAutoCancel(true)
 | 
			
		||||
                                    .setSmallIcon(R.drawable.ic_fiber_new_black_24dp)
 | 
			
		||||
 | 
			
		||||
                                Timer("", false).schedule(4000) {
 | 
			
		||||
                                    notificationManager.notify(2, newItemsNotification.build())
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        Timer("", false).schedule(4000) {
 | 
			
		||||
                            notificationManager.cancel(1)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            thread {
 | 
			
		||||
                val actions = db.actionsDao().actions()
 | 
			
		||||
 | 
			
		||||
                actions.forEach { action ->
 | 
			
		||||
                    when {
 | 
			
		||||
                        action.read -> doAndReportOnFail(api.markItem(action.articleId), action)
 | 
			
		||||
                        action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action)
 | 
			
		||||
                        action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action)
 | 
			
		||||
                        action.unstarred -> doAndReportOnFail(
 | 
			
		||||
                            api.unstarrItem(action.articleId),
 | 
			
		||||
                            action
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return Result.SUCCESS
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {
 | 
			
		||||
        call.enqueue(object : Callback<T> {
 | 
			
		||||
            override fun onResponse(
 | 
			
		||||
                call: Call<T>,
 | 
			
		||||
                response: Response<T>
 | 
			
		||||
            ) {
 | 
			
		||||
                thread {
 | 
			
		||||
                    db.actionsDao().delete(action)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            override fun onFailure(call: Call<T>, t: Throwable) {
 | 
			
		||||
                ACRA.getErrorReporter().maybeHandleSilentException(t, context)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,497 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.fragments
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.graphics.drawable.ColorDrawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import android.view.InflateException
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsIntent
 | 
			
		||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
 | 
			
		||||
import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.core.widget.NestedScrollView
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.MenuItem
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.webkit.WebSettings
 | 
			
		||||
import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import androidx.room.Room
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.maybeHandleSilentException
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.openItemUrl
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.shareLink
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.succeeded
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_article.view.*
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import retrofit2.Call
 | 
			
		||||
import retrofit2.Callback
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
import java.net.MalformedURLException
 | 
			
		||||
import java.net.URL
 | 
			
		||||
import kotlin.concurrent.thread
 | 
			
		||||
 | 
			
		||||
class ArticleFragment : Fragment() {
 | 
			
		||||
    private lateinit var pageNumber: Number
 | 
			
		||||
    private var fontSize: Int = 16
 | 
			
		||||
    private lateinit var allItems: ArrayList<Item>
 | 
			
		||||
    private var mCustomTabActivityHelper: CustomTabActivityHelper? = null;
 | 
			
		||||
    private lateinit var url: String
 | 
			
		||||
    private lateinit var contentText: String
 | 
			
		||||
    private lateinit var contentSource: String
 | 
			
		||||
    private lateinit var contentImage: String
 | 
			
		||||
    private lateinit var contentTitle: String
 | 
			
		||||
    private lateinit var editor: SharedPreferences.Editor
 | 
			
		||||
    private lateinit var fab: FloatingActionButton
 | 
			
		||||
    private lateinit var appColors: AppColors
 | 
			
		||||
    private lateinit var db: AppDatabase
 | 
			
		||||
    private lateinit var textAlignment: String
 | 
			
		||||
 | 
			
		||||
    override fun onStop() {
 | 
			
		||||
        super.onStop()
 | 
			
		||||
        if (mCustomTabActivityHelper != null) {
 | 
			
		||||
            mCustomTabActivityHelper!!.unbindCustomTabsService(activity)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        appColors = AppColors(activity!!)
 | 
			
		||||
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        pageNumber = arguments!!.getInt(ARG_POSITION)
 | 
			
		||||
        allItems = arguments!!.getParcelableArrayList(ARG_ITEMS)
 | 
			
		||||
 | 
			
		||||
        db = Room.databaseBuilder(
 | 
			
		||||
            context!!,
 | 
			
		||||
            AppDatabase::class.java, "selfoss-database"
 | 
			
		||||
        ).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).build()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var rootView: ViewGroup? = null
 | 
			
		||||
 | 
			
		||||
    private lateinit var prefs: SharedPreferences
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
        savedInstanceState: Bundle?
 | 
			
		||||
    ): View? {
 | 
			
		||||
        try {
 | 
			
		||||
            rootView = inflater
 | 
			
		||||
                .inflate(R.layout.fragment_article, container, false) as ViewGroup
 | 
			
		||||
 | 
			
		||||
            url = allItems[pageNumber.toInt()].getLinkDecoded()
 | 
			
		||||
            contentText = allItems[pageNumber.toInt()].content
 | 
			
		||||
            contentTitle = allItems[pageNumber.toInt()].title
 | 
			
		||||
            contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
 | 
			
		||||
            contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
 | 
			
		||||
 | 
			
		||||
            prefs = PreferenceManager.getDefaultSharedPreferences(activity)
 | 
			
		||||
            editor = prefs.edit()
 | 
			
		||||
            fontSize = prefs.getString("reader_font_size", "16").toInt()
 | 
			
		||||
            refreshAlignment()
 | 
			
		||||
 | 
			
		||||
            val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
            val debugReadingItems = prefs.getBoolean("read_debug", false)
 | 
			
		||||
 | 
			
		||||
            val api = SelfossApi(
 | 
			
		||||
                context!!,
 | 
			
		||||
                activity!!,
 | 
			
		||||
                settings.getBoolean("isSelfSignedCert", false),
 | 
			
		||||
                prefs.getString("api_timeout", "-1").toLong(),
 | 
			
		||||
                prefs.getBoolean("should_log_everything", false)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            fab = rootView!!.fab
 | 
			
		||||
 | 
			
		||||
            fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            fab.rippleColor = appColors.colorAccentDark
 | 
			
		||||
 | 
			
		||||
            val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar
 | 
			
		||||
            floatingToolbar.attachFab(fab)
 | 
			
		||||
 | 
			
		||||
            floatingToolbar.background = ColorDrawable(appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
            val customTabsIntent = activity!!.buildCustomTabsIntent()
 | 
			
		||||
            mCustomTabActivityHelper = CustomTabActivityHelper()
 | 
			
		||||
            mCustomTabActivityHelper!!.bindCustomTabsService(activity)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            floatingToolbar.setClickListener(
 | 
			
		||||
                object : FloatingToolbar.ItemClickListener {
 | 
			
		||||
                    override fun onItemClick(item: MenuItem) {
 | 
			
		||||
                        when (item.itemId) {
 | 
			
		||||
                            R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
 | 
			
		||||
                            R.id.share_action -> activity!!.shareLink(url, contentTitle)
 | 
			
		||||
                            R.id.open_action -> activity!!.openItemUrl(
 | 
			
		||||
                                allItems,
 | 
			
		||||
                                pageNumber.toInt(),
 | 
			
		||||
                                url,
 | 
			
		||||
                                customTabsIntent,
 | 
			
		||||
                                false,
 | 
			
		||||
                                false,
 | 
			
		||||
                                activity!!
 | 
			
		||||
                            )
 | 
			
		||||
                            R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
 | 
			
		||||
                                api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue(
 | 
			
		||||
                                    object : Callback<SuccessResponse> {
 | 
			
		||||
                                        override fun onResponse(
 | 
			
		||||
                                            call: Call<SuccessResponse>,
 | 
			
		||||
                                            response: Response<SuccessResponse>
 | 
			
		||||
                                        ) {
 | 
			
		||||
                                            if (!response.succeeded() && debugReadingItems) {
 | 
			
		||||
                                                val message =
 | 
			
		||||
                                                    "message: ${response.message()} " +
 | 
			
		||||
                                                            "response isSuccess: ${response.isSuccessful} " +
 | 
			
		||||
                                                            "response code: ${response.code()} " +
 | 
			
		||||
                                                            "response message: ${response.message()} " +
 | 
			
		||||
                                                            "response errorBody: ${response.errorBody()?.string()} " +
 | 
			
		||||
                                                            "body success: ${response.body()?.success} " +
 | 
			
		||||
                                                            "body isSuccess: ${response.body()?.isSuccess}"
 | 
			
		||||
                                                ACRA.getErrorReporter().maybeHandleSilentException(Exception(message), activity!!)
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
 | 
			
		||||
                                        override fun onFailure(
 | 
			
		||||
                                            call: Call<SuccessResponse>,
 | 
			
		||||
                                            t: Throwable
 | 
			
		||||
                                        ) {
 | 
			
		||||
                                            if (debugReadingItems) {
 | 
			
		||||
                                                ACRA.getErrorReporter().maybeHandleSilentException(t, activity!!)
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }
 | 
			
		||||
                                )
 | 
			
		||||
                            } else {
 | 
			
		||||
                                thread {
 | 
			
		||||
                                    db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            else -> Unit
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onItemLongClick(item: MenuItem?) {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            rootView!!.source.text = contentSource
 | 
			
		||||
 | 
			
		||||
            if (contentText.isEmptyOrNullOrNullString()) {
 | 
			
		||||
                getContentFromMercury(customTabsIntent, prefs)
 | 
			
		||||
            } else {
 | 
			
		||||
                rootView!!.titleView.text = contentTitle
 | 
			
		||||
 | 
			
		||||
                htmlToWebview()
 | 
			
		||||
 | 
			
		||||
                if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
 | 
			
		||||
                    rootView!!.imageView.visibility = View.VISIBLE
 | 
			
		||||
                    Glide
 | 
			
		||||
                        .with(context!!)
 | 
			
		||||
                        .asBitmap()
 | 
			
		||||
                        .load(contentImage)
 | 
			
		||||
                        .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                        .into(rootView!!.imageView)
 | 
			
		||||
                } else {
 | 
			
		||||
                    rootView!!.imageView.visibility = View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            rootView!!.nestedScrollView.setOnScrollChangeListener(
 | 
			
		||||
                NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
 | 
			
		||||
                    if (scrollY > oldScrollY) {
 | 
			
		||||
                        fab.hide()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        } catch (e: InflateException) {
 | 
			
		||||
            AlertDialog.Builder(context!!)
 | 
			
		||||
                .setMessage(context!!.getString(R.string.webview_dialog_issue_message))
 | 
			
		||||
                .setTitle(context!!.getString(R.string.webview_dialog_issue_title))
 | 
			
		||||
                .setPositiveButton(android.R.string.ok
 | 
			
		||||
                ) { dialog, which ->
 | 
			
		||||
                    val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!)
 | 
			
		||||
                    val editor = sharedPref.edit()
 | 
			
		||||
                    editor.putBoolean("prefer_article_viewer", false)
 | 
			
		||||
                    editor.commit()
 | 
			
		||||
                    activity!!.finish()
 | 
			
		||||
                }
 | 
			
		||||
                .create()
 | 
			
		||||
                .show()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return rootView
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun refreshAlignment() {
 | 
			
		||||
        textAlignment = when (prefs.getInt("text_align", 1)) {
 | 
			
		||||
            1 -> "justify"
 | 
			
		||||
            2 -> "left"
 | 
			
		||||
            else -> "justify"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getContentFromMercury(
 | 
			
		||||
        customTabsIntent: CustomTabsIntent,
 | 
			
		||||
        prefs: SharedPreferences
 | 
			
		||||
    ) {
 | 
			
		||||
        if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
 | 
			
		||||
            rootView!!.progressBar.visibility = View.VISIBLE
 | 
			
		||||
            val parser = MercuryApi(
 | 
			
		||||
                prefs.getBoolean("should_log_everything", false)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            parser.parseUrl(url).enqueue(
 | 
			
		||||
                object : Callback<ParsedContent> {
 | 
			
		||||
                    override fun onResponse(
 | 
			
		||||
                        call: Call<ParsedContent>,
 | 
			
		||||
                        response: Response<ParsedContent>
 | 
			
		||||
                    ) {
 | 
			
		||||
                        // TODO: clean all the following after finding the mercury content issue
 | 
			
		||||
                        try {
 | 
			
		||||
                            if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
 | 
			
		||||
                                try {
 | 
			
		||||
                                    rootView!!.titleView.text = response.body()!!.title
 | 
			
		||||
                                    try {
 | 
			
		||||
                                        // Note: Mercury may return relative urls... If it does the url val will not be changed.
 | 
			
		||||
                                        URL(response.body()!!.url)
 | 
			
		||||
                                        url = response.body()!!.url
 | 
			
		||||
                                    } catch (e: MalformedURLException) {
 | 
			
		||||
                                        // Mercury returned a relative url. We do nothing.
 | 
			
		||||
                                    }
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                        ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    contentText = response.body()!!.content.orEmpty()
 | 
			
		||||
                                    htmlToWebview()
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                        ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
 | 
			
		||||
                                        rootView!!.imageView.visibility = View.VISIBLE
 | 
			
		||||
                                        try {
 | 
			
		||||
                                            Glide
 | 
			
		||||
                                                .with(context!!)
 | 
			
		||||
                                                .asBitmap()
 | 
			
		||||
                                                .load(response.body()!!.lead_image_url)
 | 
			
		||||
                                                .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
                                                .into(rootView!!.imageView)
 | 
			
		||||
                                        } catch (e: Exception) {
 | 
			
		||||
                                            ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        rootView!!.imageView.visibility = View.GONE
 | 
			
		||||
                                    }
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                        ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                try {
 | 
			
		||||
                                    rootView!!.nestedScrollView.scrollTo(0, 0)
 | 
			
		||||
 | 
			
		||||
                                    rootView!!.progressBar.visibility = View.GONE
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                        ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                try {
 | 
			
		||||
                                    openInBrowserAfterFailing(customTabsIntent)
 | 
			
		||||
                                } catch (e: Exception) {
 | 
			
		||||
                                    if (context != null) {
 | 
			
		||||
                                        ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        } catch (e: Exception) {
 | 
			
		||||
                            if (context != null) {
 | 
			
		||||
                                ACRA.getErrorReporter().maybeHandleSilentException(e, context!!)
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    override fun onFailure(
 | 
			
		||||
                        call: Call<ParsedContent>,
 | 
			
		||||
                        t: Throwable
 | 
			
		||||
                    ) = openInBrowserAfterFailing(customTabsIntent)
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun htmlToWebview() {
 | 
			
		||||
        val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
 | 
			
		||||
 | 
			
		||||
        rootView!!.webcontent.visibility = View.VISIBLE
 | 
			
		||||
        val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
 | 
			
		||||
            if (context != null) {
 | 
			
		||||
                rootView!!.webcontent.setBackgroundColor(
 | 
			
		||||
                    ContextCompat.getColor(
 | 
			
		||||
                        context!!,
 | 
			
		||||
                        R.color.dark_webview
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                Pair(ContextCompat.getColor(context!!, R.color.dark_webview_text), ContextCompat.getColor(context!!, R.color.light_webview_text))
 | 
			
		||||
            } else {
 | 
			
		||||
                Pair(null, null)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            if (context != null) {
 | 
			
		||||
                rootView!!.webcontent.setBackgroundColor(
 | 
			
		||||
                    ContextCompat.getColor(
 | 
			
		||||
                        context!!,
 | 
			
		||||
                        R.color.light_webview
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                Pair(ContextCompat.getColor(context!!, R.color.light_webview_text), ContextCompat.getColor(context!!, R.color.dark_webview_text))
 | 
			
		||||
            } else {
 | 
			
		||||
                Pair(null, null)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val stringTextColor: String = if (textColor != null) {
 | 
			
		||||
            String.format("#%06X", 0xFFFFFF and textColor)
 | 
			
		||||
        } else {
 | 
			
		||||
            "#000000"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val stringBackgroundColor = if (backgroundColor != null) {
 | 
			
		||||
            String.format("#%06X", 0xFFFFFF and backgroundColor)
 | 
			
		||||
        } else {
 | 
			
		||||
            "#FFFFFF"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        rootView!!.webcontent.settings.useWideViewPort = true
 | 
			
		||||
        rootView!!.webcontent.settings.loadWithOverviewMode = true
 | 
			
		||||
        rootView!!.webcontent.settings.javaScriptEnabled = false
 | 
			
		||||
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
 | 
			
		||||
            rootView!!.webcontent.settings.layoutAlgorithm =
 | 
			
		||||
                    WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
 | 
			
		||||
        } else {
 | 
			
		||||
            rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var baseUrl: String? = null
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            val itemUrl = URL(url)
 | 
			
		||||
            baseUrl = itemUrl.protocol + "://" + itemUrl.host
 | 
			
		||||
        } catch (e: MalformedURLException) {
 | 
			
		||||
            ACRA.getErrorReporter().maybeHandleSilentException(e, activity!!)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        rootView!!.webcontent.loadDataWithBaseURL(
 | 
			
		||||
            baseUrl,
 | 
			
		||||
            """<html>
 | 
			
		||||
                |<head>
 | 
			
		||||
                |   <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
                |   <style>
 | 
			
		||||
                |      img {
 | 
			
		||||
                |        display: inline-block;
 | 
			
		||||
                |        height: auto;
 | 
			
		||||
                |        width: 100%;
 | 
			
		||||
                |        max-width: 100%;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      a {
 | 
			
		||||
                |        color: $stringColor !important;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      *:not(a) {
 | 
			
		||||
                |        color: $stringTextColor;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      * {
 | 
			
		||||
                |        font-size: ${fontSize}px;
 | 
			
		||||
                |        text-align: $textAlignment;
 | 
			
		||||
                |        word-break: break-word;
 | 
			
		||||
                |        overflow:hidden;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      a, pre, code {
 | 
			
		||||
                |        text-align: $textAlignment;
 | 
			
		||||
                |      }
 | 
			
		||||
                |      pre, code {
 | 
			
		||||
                |        white-space: pre-wrap;
 | 
			
		||||
                |        width:100%;
 | 
			
		||||
                |        background-color: $stringBackgroundColor;
 | 
			
		||||
                |      }
 | 
			
		||||
                |   </style>
 | 
			
		||||
                |</head>
 | 
			
		||||
                |<body>
 | 
			
		||||
                |   $contentText
 | 
			
		||||
                |</body>""".trimMargin(),
 | 
			
		||||
            "text/html",
 | 
			
		||||
            "utf-8",
 | 
			
		||||
            null
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
 | 
			
		||||
        rootView!!.progressBar.visibility = View.GONE
 | 
			
		||||
        activity!!.openItemUrl(
 | 
			
		||||
            allItems,
 | 
			
		||||
            pageNumber.toInt(),
 | 
			
		||||
            url,
 | 
			
		||||
            customTabsIntent,
 | 
			
		||||
            true,
 | 
			
		||||
            false,
 | 
			
		||||
            activity!!
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private const val ARG_POSITION = "position"
 | 
			
		||||
        private const val ARG_ITEMS = "items"
 | 
			
		||||
 | 
			
		||||
        fun newInstance(
 | 
			
		||||
            position: Int,
 | 
			
		||||
            allItems: ArrayList<Item>
 | 
			
		||||
        ): ArticleFragment {
 | 
			
		||||
            val fragment = ArticleFragment()
 | 
			
		||||
            val args = Bundle()
 | 
			
		||||
            args.putInt(ARG_POSITION, position)
 | 
			
		||||
            args.putParcelableArrayList(ARG_ITEMS, allItems)
 | 
			
		||||
            fragment.arguments = args
 | 
			
		||||
            return fragment
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.dao
 | 
			
		||||
 | 
			
		||||
import androidx.room.Dao
 | 
			
		||||
import androidx.room.Delete
 | 
			
		||||
import androidx.room.Insert
 | 
			
		||||
import androidx.room.OnConflictStrategy
 | 
			
		||||
import androidx.room.Query
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
 | 
			
		||||
@Dao
 | 
			
		||||
interface ActionsDao {
 | 
			
		||||
    @Query("SELECT * FROM actions order by id asc")
 | 
			
		||||
    fun actions(): List<ActionEntity>
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    fun insertAllActions(vararg actions: ActionEntity)
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM actions WHERE articleid = :article_id AND read = 1")
 | 
			
		||||
    fun deleteReadActionForArticle(article_id: String)
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    fun delete(action: ActionEntity)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.dao
 | 
			
		||||
 | 
			
		||||
import androidx.room.Delete
 | 
			
		||||
import androidx.room.Dao
 | 
			
		||||
import androidx.room.Insert
 | 
			
		||||
import androidx.room.OnConflictStrategy
 | 
			
		||||
import androidx.room.Query
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
 | 
			
		||||
 | 
			
		||||
@Dao
 | 
			
		||||
interface DrawerDataDao {
 | 
			
		||||
    @Query("SELECT * FROM tags")
 | 
			
		||||
    fun tags(): List<TagEntity>
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT * FROM sources")
 | 
			
		||||
    fun sources(): List<SourceEntity>
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    fun insertAllTags(vararg tags: TagEntity)
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    fun insertAllSources(vararg sources: SourceEntity)
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM tags")
 | 
			
		||||
    fun deleteAllTags()
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM sources")
 | 
			
		||||
    fun deleteAllSources()
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    fun deleteTag(tag: TagEntity)
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    fun deleteSource(source: SourceEntity)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.dao
 | 
			
		||||
 | 
			
		||||
import androidx.room.Dao
 | 
			
		||||
import androidx.room.Delete
 | 
			
		||||
import androidx.room.Insert
 | 
			
		||||
import androidx.room.OnConflictStrategy
 | 
			
		||||
import androidx.room.Query
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
 | 
			
		||||
import androidx.room.Update
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@Dao
 | 
			
		||||
interface ItemsDao {
 | 
			
		||||
    @Query("SELECT * FROM items order by id desc")
 | 
			
		||||
    fun items(): List<ItemEntity>
 | 
			
		||||
 | 
			
		||||
    @Insert(onConflict = OnConflictStrategy.REPLACE)
 | 
			
		||||
    fun insertAllItems(vararg items: ItemEntity)
 | 
			
		||||
 | 
			
		||||
    @Query("DELETE FROM items")
 | 
			
		||||
    fun deleteAllItems()
 | 
			
		||||
 | 
			
		||||
    @Delete
 | 
			
		||||
    fun delete(item: ItemEntity)
 | 
			
		||||
 | 
			
		||||
    @Update
 | 
			
		||||
    fun updateItem(item: ItemEntity)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.database
 | 
			
		||||
 | 
			
		||||
import androidx.room.RoomDatabase
 | 
			
		||||
import androidx.room.Database
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.dao.ActionsDao
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.dao.DrawerDataDao
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.dao.ItemsDao
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
 | 
			
		||||
 | 
			
		||||
@Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 3)
 | 
			
		||||
abstract class AppDatabase : RoomDatabase() {
 | 
			
		||||
    abstract fun drawerDataDao(): DrawerDataDao
 | 
			
		||||
 | 
			
		||||
    abstract fun itemsDao(): ItemsDao
 | 
			
		||||
 | 
			
		||||
    abstract fun actionsDao(): ActionsDao
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.entities
 | 
			
		||||
 | 
			
		||||
import androidx.room.ColumnInfo
 | 
			
		||||
import androidx.room.Entity
 | 
			
		||||
import androidx.room.PrimaryKey
 | 
			
		||||
 | 
			
		||||
@Entity(tableName = "actions")
 | 
			
		||||
data class ActionEntity(
 | 
			
		||||
    @ColumnInfo(name = "articleid")
 | 
			
		||||
    val articleId: String,
 | 
			
		||||
    @ColumnInfo(name = "read")
 | 
			
		||||
    val read: Boolean,
 | 
			
		||||
    @ColumnInfo(name = "unread")
 | 
			
		||||
    val unread: Boolean,
 | 
			
		||||
    @ColumnInfo(name = "starred")
 | 
			
		||||
    var starred: Boolean,
 | 
			
		||||
    @ColumnInfo(name = "unstarred")
 | 
			
		||||
    var unstarred: Boolean
 | 
			
		||||
) {
 | 
			
		||||
    @PrimaryKey(autoGenerate = true)
 | 
			
		||||
    var id: Int = 0
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.entities
 | 
			
		||||
 | 
			
		||||
import androidx.room.ColumnInfo
 | 
			
		||||
import androidx.room.Entity
 | 
			
		||||
import androidx.room.PrimaryKey
 | 
			
		||||
 | 
			
		||||
@Entity(tableName = "tags")
 | 
			
		||||
data class TagEntity(
 | 
			
		||||
    @PrimaryKey
 | 
			
		||||
    @ColumnInfo(name = "tag")
 | 
			
		||||
    val tag: String,
 | 
			
		||||
    @ColumnInfo(name = "color")
 | 
			
		||||
    val color: String,
 | 
			
		||||
    @ColumnInfo(name = "unread")
 | 
			
		||||
    val unread: Int
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Entity(tableName = "sources")
 | 
			
		||||
data class SourceEntity(
 | 
			
		||||
    @PrimaryKey
 | 
			
		||||
    @ColumnInfo(name = "id")
 | 
			
		||||
    val id: String,
 | 
			
		||||
    @ColumnInfo(name = "title")
 | 
			
		||||
    val title: String,
 | 
			
		||||
    @ColumnInfo(name = "tags")
 | 
			
		||||
    val tags: String,
 | 
			
		||||
    @ColumnInfo(name = "spout")
 | 
			
		||||
    val spout: String,
 | 
			
		||||
    @ColumnInfo(name = "error")
 | 
			
		||||
    val error: String,
 | 
			
		||||
    @ColumnInfo(name = "icon")
 | 
			
		||||
    val icon: String
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.entities
 | 
			
		||||
 | 
			
		||||
import androidx.room.ColumnInfo
 | 
			
		||||
import androidx.room.Entity
 | 
			
		||||
import androidx.room.PrimaryKey
 | 
			
		||||
 | 
			
		||||
@Entity(tableName = "items")
 | 
			
		||||
data class ItemEntity(
 | 
			
		||||
    @PrimaryKey
 | 
			
		||||
    @ColumnInfo(name = "id")
 | 
			
		||||
    val id: String,
 | 
			
		||||
    @ColumnInfo(name = "datetime")
 | 
			
		||||
    val datetime: String,
 | 
			
		||||
    @ColumnInfo(name = "title")
 | 
			
		||||
    val title: String,
 | 
			
		||||
    @ColumnInfo(name = "content")
 | 
			
		||||
    val content: String,
 | 
			
		||||
    @ColumnInfo(name = "unread")
 | 
			
		||||
    val unread: Boolean,
 | 
			
		||||
    @ColumnInfo(name = "starred")
 | 
			
		||||
    var starred: Boolean,
 | 
			
		||||
    @ColumnInfo(name = "thumbnail")
 | 
			
		||||
    val thumbnail: String,
 | 
			
		||||
    @ColumnInfo(name = "icon")
 | 
			
		||||
    val icon: String,
 | 
			
		||||
    @ColumnInfo(name = "link")
 | 
			
		||||
    val link: String,
 | 
			
		||||
    @ColumnInfo(name = "sourcetitle")
 | 
			
		||||
    val sourcetitle: String,
 | 
			
		||||
    @ColumnInfo(name = "tags")
 | 
			
		||||
    val tags: String
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.persistence.migrations
 | 
			
		||||
 | 
			
		||||
import androidx.sqlite.db.SupportSQLiteDatabase
 | 
			
		||||
import androidx.room.migration.Migration
 | 
			
		||||
 | 
			
		||||
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
 | 
			
		||||
    override fun migrate(database: SupportSQLiteDatabase) {
 | 
			
		||||
        database.execSQL("CREATE TABLE IF NOT EXISTS `items` (`id` TEXT NOT NULL, `datetime` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `thumbnail` TEXT NOT NULL, `icon` TEXT NOT NULL, `link` TEXT NOT NULL, `sourcetitle` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`id`))")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val MIGRATION_2_3: Migration = object : Migration(2, 3) {
 | 
			
		||||
    override fun migrate(database: SupportSQLiteDatabase) {
 | 
			
		||||
        database.execSQL("CREATE TABLE IF NOT EXISTS `actions` (`id` INTEGER NOT NULL, `articleid` TEXT NOT NULL, `read` INTEGER NOT NULL, `unread` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `unstarred` INTEGER NOT NULL, PRIMARY KEY(`id`))")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,28 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.settings;
 | 
			
		||||
 | 
			
		||||
import android.content.res.Configuration;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.preference.PreferenceActivity;
 | 
			
		||||
import android.support.annotation.LayoutRes;
 | 
			
		||||
import android.support.annotation.NonNull;
 | 
			
		||||
import android.support.annotation.Nullable;
 | 
			
		||||
import android.support.v7.app.ActionBar;
 | 
			
		||||
import android.support.v7.app.AppCompatDelegate;
 | 
			
		||||
import android.support.v7.widget.Toolbar;
 | 
			
		||||
import androidx.annotation.LayoutRes;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import com.google.android.material.appbar.AppBarLayout;
 | 
			
		||||
import androidx.appcompat.app.ActionBar;
 | 
			
		||||
import androidx.appcompat.app.AppCompatDelegate;
 | 
			
		||||
import androidx.appcompat.widget.Toolbar;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.MenuInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.LinearLayout;
 | 
			
		||||
 | 
			
		||||
import com.ftinc.scoop.Scoop;
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R;
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors;
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.Toppings;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A {@link PreferenceActivity} which implements and proxies the necessary calls
 | 
			
		||||
@@ -23,6 +34,8 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        new AppColors(this);
 | 
			
		||||
 | 
			
		||||
        getDelegate().installViewFactory();
 | 
			
		||||
        getDelegate().onCreate(savedInstanceState);
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
@@ -31,6 +44,23 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onPostCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onPostCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        LinearLayout root = (LinearLayout) findViewById(android.R.id.list).getParent().getParent().getParent();
 | 
			
		||||
        AppBarLayout bar = (AppBarLayout) LayoutInflater.from(this).inflate(R.layout.settings_toolbar, root, false);
 | 
			
		||||
        Toolbar toolbar = bar.findViewById(R.id.toolbar);
 | 
			
		||||
 | 
			
		||||
        Scoop scoop = Scoop.getInstance();
 | 
			
		||||
        scoop.bind(this, Toppings.PRIMARY.getValue(), toolbar);
 | 
			
		||||
        if  (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
 | 
			
		||||
            scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.getValue());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSupportActionBar(toolbar);
 | 
			
		||||
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 | 
			
		||||
        getSupportActionBar().setDisplayShowHomeEnabled(true);
 | 
			
		||||
 | 
			
		||||
        root.addView(bar, 0);
 | 
			
		||||
 | 
			
		||||
        getDelegate().onPostCreate(savedInstanceState);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -98,6 +128,7 @@ public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
 | 
			
		||||
        getDelegate().onDestroy();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void invalidateOptionsMenu() {
 | 
			
		||||
        getDelegate().invalidateOptionsMenu();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,24 +2,38 @@ package apps.amine.bou.readerforselfoss.settings;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.annotation.TargetApi;
 | 
			
		||||
import android.content.ClipData;
 | 
			
		||||
import android.content.ClipboardManager;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.content.res.Configuration;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Build;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.preference.EditTextPreference;
 | 
			
		||||
import android.preference.Preference;
 | 
			
		||||
import android.preference.Preference.OnPreferenceChangeListener;
 | 
			
		||||
import android.preference.Preference.OnPreferenceClickListener;
 | 
			
		||||
import android.preference.PreferenceActivity;
 | 
			
		||||
import android.preference.SwitchPreference;
 | 
			
		||||
import android.support.v7.app.ActionBar;
 | 
			
		||||
import android.preference.PreferenceFragment;
 | 
			
		||||
import android.preference.PreferenceManager;
 | 
			
		||||
import android.preference.SwitchPreference;
 | 
			
		||||
import androidx.appcompat.app.ActionBar;
 | 
			
		||||
import android.text.Editable;
 | 
			
		||||
import android.text.InputFilter;
 | 
			
		||||
import android.text.Spanned;
 | 
			
		||||
import android.text.TextWatcher;
 | 
			
		||||
import android.view.Menu;
 | 
			
		||||
import android.view.MenuInflater;
 | 
			
		||||
import android.view.MenuItem;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R;
 | 
			
		||||
import apps.amine.bou.readerforselfoss.themes.AppColors;
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -79,6 +93,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        new AppColors(this);
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        setupActionBar();
 | 
			
		||||
    }
 | 
			
		||||
@@ -115,10 +130,16 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
     * This method stops fragment injection in malicious applications.
 | 
			
		||||
     * Make sure to deny any unknown fragments here.
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean isValidFragment(String fragmentName) {
 | 
			
		||||
        return PreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || GeneralPreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || LinksPreferenceFragment.class.getName().equals(fragmentName);
 | 
			
		||||
                || ArticleViewerPreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || OfflinePreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || ExperimentalPreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || DebugPreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || LinksPreferenceFragment.class.getName().equals(fragmentName)
 | 
			
		||||
                || ThemePreferenceFragment.class.getName().equals(fragmentName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -133,16 +154,116 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_general);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
 | 
			
		||||
            SwitchPreference cardViewActive = (SwitchPreference) findPreference("card_view_active");
 | 
			
		||||
            final SwitchPreference tabOnTap = (SwitchPreference) findPreference("tab_on_tap");
 | 
			
		||||
            tabOnTap.setEnabled(!cardViewActive.isChecked());
 | 
			
		||||
            cardViewActive.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
 | 
			
		||||
                public boolean onPreferenceChange(Preference preference, Object newValue){
 | 
			
		||||
                    boolean isEnabled = (Boolean) newValue;
 | 
			
		||||
                    tabOnTap.setEnabled(!isEnabled);
 | 
			
		||||
                    return true;
 | 
			
		||||
            EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
 | 
			
		||||
            itemsNumber.getEditText().setFilters(new InputFilter[]{
 | 
			
		||||
                    new InputFilter() {
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
 | 
			
		||||
                            try {
 | 
			
		||||
                                int input = Integer.parseInt(dest.toString() + source.toString());
 | 
			
		||||
                                if (input <= 200 && input > 0)
 | 
			
		||||
                                    return null;
 | 
			
		||||
                            } catch (NumberFormatException nfe) {
 | 
			
		||||
                                Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show();
 | 
			
		||||
                            }
 | 
			
		||||
                            return "";
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
            int id = item.getItemId();
 | 
			
		||||
            if (id == android.R.id.home) {
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class ArticleViewerPreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_viewer);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
 | 
			
		||||
            final EditTextPreference fontSize = (EditTextPreference) findPreference("reader_font_size");
 | 
			
		||||
            fontSize.getEditText().addTextChangedListener(new TextWatcher() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void afterTextChanged(Editable editable) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
 | 
			
		||||
                    } catch (NumberFormatException e) {}
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            fontSize.getEditText().setFilters(new InputFilter[]{
 | 
			
		||||
                    new InputFilter() {
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
 | 
			
		||||
                            try {
 | 
			
		||||
                                int input = Integer.parseInt(dest.toString() + source.toString());
 | 
			
		||||
                                if (input > 0)
 | 
			
		||||
                                    return null;
 | 
			
		||||
                            } catch (NumberFormatException nfe) {}
 | 
			
		||||
                            return "";
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
            int id = item.getItemId();
 | 
			
		||||
            if (id == android.R.id.home) {
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class DebugPreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_debug);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
 | 
			
		||||
            SharedPreferences pref = getActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE);
 | 
			
		||||
            final String id = pref.getString("unique_id", "...");
 | 
			
		||||
 | 
			
		||||
            final Preference identifier = findPreference("debug_identifier");
 | 
			
		||||
            final ClipboardManager clipboard = (ClipboardManager)
 | 
			
		||||
                    getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
 | 
			
		||||
 | 
			
		||||
            identifier.setOnPreferenceClickListener(new OnPreferenceClickListener() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
                    if (clipboard != null) {
 | 
			
		||||
                        ClipData clip = ClipData.newPlainText("Selfoss unique id", id);
 | 
			
		||||
                        clipboard.setPrimaryClip(clip);
 | 
			
		||||
 | 
			
		||||
                        Toast.makeText(getActivity(), R.string.unique_id_to_clipboard, Toast.LENGTH_LONG).show();
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            identifier.setTitle(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
@@ -162,18 +283,21 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
     */
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class LinksPreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        public void openUrl(Uri uri) {
 | 
			
		||||
            Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
 | 
			
		||||
            startActivity(browserIntent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_links);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
 | 
			
		||||
            Preference tracker = findPreference( "trackerLink" );
 | 
			
		||||
            tracker.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 | 
			
		||||
            findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
                    Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.tracker_url)));
 | 
			
		||||
                    startActivity(browserIntent);
 | 
			
		||||
                    openUrl(Uri.parse(Config.trackerUrl));
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@@ -181,8 +305,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
            findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
                    Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.source_url)));
 | 
			
		||||
                    startActivity(browserIntent);
 | 
			
		||||
                    openUrl(Uri.parse(Config.sourceUrl));
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
                    openUrl(Uri.parse(Config.translationUrl));
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
@@ -199,6 +330,82 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class ThemePreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_theme);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
            int id = item.getItemId();
 | 
			
		||||
            if (id == android.R.id.home) {
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
                return true;
 | 
			
		||||
            } else if (id == R.id.clear) {
 | 
			
		||||
                SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity());
 | 
			
		||||
                SharedPreferences.Editor editor = pref.edit();
 | 
			
		||||
                editor.remove("color_primary");
 | 
			
		||||
                editor.remove("color_primary_dark");
 | 
			
		||||
                editor.remove("color_accent");
 | 
			
		||||
                editor.remove("color_accent_dark");
 | 
			
		||||
                editor.remove("dark_theme");
 | 
			
		||||
                editor.apply();
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
 | 
			
		||||
            inflater.inflate(R.menu.settings_theme, menu);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class OfflinePreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_offline);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
            int id = item.getItemId();
 | 
			
		||||
            if (id == android.R.id.home) {
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
 | 
			
		||||
    public static class ExperimentalPreferenceFragment extends PreferenceFragment {
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
            super.onCreate(savedInstanceState);
 | 
			
		||||
            addPreferencesFromResource(R.xml.pref_experimental);
 | 
			
		||||
            setHasOptionsMenu(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
            int id = item.getItemId();
 | 
			
		||||
            if (id == android.R.id.home) {
 | 
			
		||||
                getActivity().finish();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return super.onOptionsItemSelected(item);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onOptionsItemSelected(MenuItem item) {
 | 
			
		||||
        int id = item.getItemId();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.themes
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import androidx.annotation.ColorInt
 | 
			
		||||
import androidx.appcompat.view.ContextThemeWrapper
 | 
			
		||||
import android.util.TypedValue
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
 | 
			
		||||
class AppColors(a: Activity) {
 | 
			
		||||
 | 
			
		||||
    @ColorInt val colorPrimary: Int
 | 
			
		||||
    @ColorInt val colorPrimaryDark: Int
 | 
			
		||||
    @ColorInt val colorAccent: Int
 | 
			
		||||
    @ColorInt val colorAccentDark: Int
 | 
			
		||||
    @ColorInt val cardBackgroundColor: Int
 | 
			
		||||
    @ColorInt val colorBackground: Int
 | 
			
		||||
    val isDarkTheme: Boolean
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val sharedPref = PreferenceManager.getDefaultSharedPreferences(a)
 | 
			
		||||
 | 
			
		||||
        colorPrimary =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_primary",
 | 
			
		||||
                    a.resources.getColor(R.color.colorPrimary)
 | 
			
		||||
                )
 | 
			
		||||
        colorPrimaryDark =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_primary_dark",
 | 
			
		||||
                    a.resources.getColor(R.color.colorPrimaryDark)
 | 
			
		||||
                )
 | 
			
		||||
        colorAccent =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_accent",
 | 
			
		||||
                    a.resources.getColor(R.color.colorAccent)
 | 
			
		||||
                )
 | 
			
		||||
        colorAccentDark =
 | 
			
		||||
                sharedPref.getInt(
 | 
			
		||||
                    "color_accent_dark",
 | 
			
		||||
                    a.resources.getColor(R.color.colorAccentDark)
 | 
			
		||||
                )
 | 
			
		||||
        isDarkTheme =
 | 
			
		||||
                sharedPref.getBoolean(
 | 
			
		||||
                    "dark_theme",
 | 
			
		||||
                    false
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        colorBackground = if (isDarkTheme) {
 | 
			
		||||
            a.setTheme(R.style.NoBarDark)
 | 
			
		||||
            R.color.darkBackground
 | 
			
		||||
        } else {
 | 
			
		||||
            a.setTheme(R.style.NoBar)
 | 
			
		||||
            android.R.color.background_light
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val wrapper = Context::class.java
 | 
			
		||||
        val method = wrapper!!.getMethod("getThemeResId")
 | 
			
		||||
        method.isAccessible = true
 | 
			
		||||
 | 
			
		||||
        val typedCardBackground = TypedValue()
 | 
			
		||||
        a.theme.resolveAttribute(R.attr.cardBackgroundColor, typedCardBackground, true)
 | 
			
		||||
 | 
			
		||||
        cardBackgroundColor = typedCardBackground.data
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.themes
 | 
			
		||||
 | 
			
		||||
enum class Toppings(val value: Int) {
 | 
			
		||||
    PRIMARY(1),
 | 
			
		||||
    PRIMARY_DARK(2),
 | 
			
		||||
    ACCENT(3),
 | 
			
		||||
    ACCENT_DARK(4)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.transformers
 | 
			
		||||
 | 
			
		||||
import androidx.viewpager.widget.ViewPager
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class DepthPageTransformer : ViewPager.PageTransformer {
 | 
			
		||||
 | 
			
		||||
    override fun transformPage(view: View, position: Float) {
 | 
			
		||||
        val pageWidth = view.width
 | 
			
		||||
 | 
			
		||||
        when {
 | 
			
		||||
            position < -1 -> // [-Infinity,-1)
 | 
			
		||||
                // This page is way off-screen to the left.
 | 
			
		||||
                view.alpha = 0F
 | 
			
		||||
            position <= 0 -> { // [-1,0]
 | 
			
		||||
                // Use the default slide transition when moving to the left page
 | 
			
		||||
                view.alpha = 1F
 | 
			
		||||
                view.translationX = 0F
 | 
			
		||||
                view.scaleX = 1F
 | 
			
		||||
                view.scaleY = 1F
 | 
			
		||||
            }
 | 
			
		||||
            position <= 1 -> { // (0,1]
 | 
			
		||||
                // Fade the page out.
 | 
			
		||||
                view.alpha = 1 - position
 | 
			
		||||
 | 
			
		||||
                // Counteract the default slide transition
 | 
			
		||||
                view.translationX = pageWidth * -position
 | 
			
		||||
 | 
			
		||||
                // Scale the page down (between MIN_SCALE and 1)
 | 
			
		||||
                val scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position))
 | 
			
		||||
                view.scaleX = scaleFactor
 | 
			
		||||
                view.scaleY = scaleFactor
 | 
			
		||||
            }
 | 
			
		||||
            else -> // (1,+Infinity]
 | 
			
		||||
                // This page is way off-screen to the right.
 | 
			
		||||
                view.alpha = 0F
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val MIN_SCALE = 0.75f
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.preference.PreferenceManager
 | 
			
		||||
import android.provider.Settings
 | 
			
		||||
import org.acra.ErrorReporter
 | 
			
		||||
 | 
			
		||||
fun ErrorReporter.maybeHandleSilentException(throwable: Throwable, ctx: Context) {
 | 
			
		||||
    val sharedPref = PreferenceManager.getDefaultSharedPreferences(ctx)
 | 
			
		||||
    val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") ==  "true"
 | 
			
		||||
 | 
			
		||||
    if (sharedPref.getBoolean("acra_should_log", false) && !isTestLab) {
 | 
			
		||||
        this.handleSilentException(throwable)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun ErrorReporter.doHandleSilentException(throwable: Throwable, ctx: Context) {
 | 
			
		||||
    val isTestLab = Settings.System.getString(ctx.contentResolver, "firebase.test.lab") ==  "true"
 | 
			
		||||
    if (!isTestLab) {
 | 
			
		||||
        this.handleSilentException(throwable)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
 | 
			
		||||
import retrofit2.Response
 | 
			
		||||
 | 
			
		||||
fun Response<SuccessResponse>.succeeded(): Boolean =
 | 
			
		||||
    this.code() === 200 && this.body() != null && this.body()!!.isSuccess
 | 
			
		||||
@@ -2,98 +2,40 @@ package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.v7.app.AlertDialog
 | 
			
		||||
import android.text.TextUtils
 | 
			
		||||
import android.util.Patterns
 | 
			
		||||
import apps.amine.bou.readerforselfoss.BuildConfig
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
 | 
			
		||||
private fun isStoreVersion(context: Context): Boolean {
 | 
			
		||||
    var result = false
 | 
			
		||||
    try {
 | 
			
		||||
        val installer = context.packageManager
 | 
			
		||||
                .getInstallerPackageName(context.packageName)
 | 
			
		||||
        result = !TextUtils.isEmpty(installer)
 | 
			
		||||
    } catch (e: Throwable) {
 | 
			
		||||
    }
 | 
			
		||||
fun String?.isEmptyOrNullOrNullString(): Boolean =
 | 
			
		||||
    this == null || this == "null" || this.isEmpty()
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun checkAndDisplayStoreApk(context: Context) =
 | 
			
		||||
    if (!isStoreVersion(context) && !BuildConfig.GITHUB_VERSION) {
 | 
			
		||||
        val alertDialog = AlertDialog.Builder(context).create()
 | 
			
		||||
        alertDialog.setTitle(context.getString(R.string.warning_version))
 | 
			
		||||
        alertDialog.setMessage(context.getString(R.string.text_version))
 | 
			
		||||
        alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
 | 
			
		||||
                { dialog, _ -> dialog.dismiss() })
 | 
			
		||||
        alertDialog.show()
 | 
			
		||||
    } else Unit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
fun isUrlValid(url: String): Boolean {
 | 
			
		||||
    val baseUrl = HttpUrl.parse(url)
 | 
			
		||||
    var existsAndEndsWithSlash = false
 | 
			
		||||
    if (baseUrl != null) {
 | 
			
		||||
        val pathSegments = baseUrl.pathSegments()
 | 
			
		||||
        existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Patterns.WEB_URL.matcher(url).matches() && existsAndEndsWithSlash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun isEmptyOrNullOrNullString(str: String?): Boolean =
 | 
			
		||||
        str == null || str == "null" || str.isEmpty()
 | 
			
		||||
 | 
			
		||||
fun checkApkVersion(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
 | 
			
		||||
    mFirebaseRemoteConfig.fetch(43200)
 | 
			
		||||
        .addOnCompleteListener { task ->
 | 
			
		||||
            if (task.isSuccessful) {
 | 
			
		||||
                mFirebaseRemoteConfig.activateFetched()
 | 
			
		||||
            } else {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            isThereAnUpdate(settings, editor, context, mFirebaseRemoteConfig)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun isThereAnUpdate(settings: SharedPreferences, editor: SharedPreferences.Editor, context: Context, mFirebaseRemoteConfig: FirebaseRemoteConfig) {
 | 
			
		||||
    val APK_LINK = "github_apk"
 | 
			
		||||
 | 
			
		||||
    val apkLink = mFirebaseRemoteConfig.getString(APK_LINK)
 | 
			
		||||
    val storedLink = settings.getString(APK_LINK, "")
 | 
			
		||||
    if (apkLink != storedLink && !apkLink.isEmpty()) {
 | 
			
		||||
        val alertDialog = AlertDialog.Builder(context).create()
 | 
			
		||||
        alertDialog.setTitle(context.getString(R.string.new_apk_available_title))
 | 
			
		||||
        alertDialog.setMessage(context.getString(R.string.new_apk_available_message))
 | 
			
		||||
        alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.new_apk_available_get)) { _, _ ->
 | 
			
		||||
            editor.putString(APK_LINK, apkLink)
 | 
			
		||||
            editor.apply()
 | 
			
		||||
            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(apkLink))
 | 
			
		||||
            context.startActivity(browserIntent)
 | 
			
		||||
        }
 | 
			
		||||
        alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.new_apk_available_no),
 | 
			
		||||
                { dialog, _ ->
 | 
			
		||||
                    editor.putString(APK_LINK, apkLink)
 | 
			
		||||
                    editor.apply()
 | 
			
		||||
                    dialog.dismiss()
 | 
			
		||||
                })
 | 
			
		||||
        alertDialog.show()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun longHash(string: String): Long {
 | 
			
		||||
fun String.longHash(): Long {
 | 
			
		||||
    var h = 98764321261L
 | 
			
		||||
    val l = string.length
 | 
			
		||||
    val chars = string.toCharArray()
 | 
			
		||||
    val l = this.length
 | 
			
		||||
    val chars = this.toCharArray()
 | 
			
		||||
 | 
			
		||||
    for (i in 0..l - 1) {
 | 
			
		||||
    for (i in 0 until l) {
 | 
			
		||||
        h = 31 * h + chars[i].toLong()
 | 
			
		||||
    }
 | 
			
		||||
    return h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun String.toStringUriWithHttp(): String =
 | 
			
		||||
    if (!this.startsWith("https://") && !this.startsWith("http://")) {
 | 
			
		||||
        "http://" + this
 | 
			
		||||
    } else {
 | 
			
		||||
        this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
fun Context.shareLink(itemUrl: String, itemTitle: String) {
 | 
			
		||||
    val sendIntent = Intent()
 | 
			
		||||
    sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
    sendIntent.action = Intent.ACTION_SEND
 | 
			
		||||
    sendIntent.putExtra(Intent.EXTRA_TEXT, itemUrl.toStringUriWithHttp())
 | 
			
		||||
    sendIntent.putExtra(Intent.EXTRA_SUBJECT, itemTitle)
 | 
			
		||||
    sendIntent.type = "text/plain"
 | 
			
		||||
    startActivity(
 | 
			
		||||
        Intent.createChooser(
 | 
			
		||||
            sendIntent,
 | 
			
		||||
            getString(R.string.share)
 | 
			
		||||
        ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +1,14 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.LoginActivity
 | 
			
		||||
 | 
			
		||||
class Config(c: Context) {
 | 
			
		||||
 | 
			
		||||
    private val settings: SharedPreferences
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        this.settings = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
    }
 | 
			
		||||
    val settings: SharedPreferences = c.getSharedPreferences(settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
 | 
			
		||||
    val baseUrl: String
 | 
			
		||||
        get() = settings.getString("url", "")
 | 
			
		||||
@@ -28,7 +26,37 @@ class Config(c: Context) {
 | 
			
		||||
        get() = settings.getString("httpPassword", "")
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val settingsName = "paramsselfoss"
 | 
			
		||||
        const val settingsName = "paramsselfoss"
 | 
			
		||||
 | 
			
		||||
        const val feedbackEmail = "aminecmi@gmail.com"
 | 
			
		||||
 | 
			
		||||
        const val translationUrl = "https://crwd.in/readerforselfoss"
 | 
			
		||||
 | 
			
		||||
        const val sourceUrl = "https://github.com/aminecmi/ReaderforSelfoss"
 | 
			
		||||
 | 
			
		||||
        const val trackerUrl = "https://github.com/aminecmi/ReaderforSelfoss/issues"
 | 
			
		||||
 | 
			
		||||
        const val syncChannelId = "sync-channel-id"
 | 
			
		||||
 | 
			
		||||
        const val newItemsChannelId = "new-items-channel-id"
 | 
			
		||||
 | 
			
		||||
        fun logoutAndRedirect(
 | 
			
		||||
            c: Context,
 | 
			
		||||
            callingActivity: Activity,
 | 
			
		||||
            editor: SharedPreferences.Editor,
 | 
			
		||||
            baseUrlFail: Boolean = false
 | 
			
		||||
        ): Boolean {
 | 
			
		||||
            editor.remove("url")
 | 
			
		||||
            editor.remove("login")
 | 
			
		||||
            editor.remove("password")
 | 
			
		||||
            editor.apply()
 | 
			
		||||
            val intent = Intent(c, LoginActivity::class.java)
 | 
			
		||||
            if (baseUrlFail) {
 | 
			
		||||
                intent.putExtra("baseUrlFail", baseUrlFail)
 | 
			
		||||
            }
 | 
			
		||||
            c.startActivity(intent)
 | 
			
		||||
            callingActivity.finish()
 | 
			
		||||
            return true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import java.security.cert.CertificateException
 | 
			
		||||
import java.security.cert.X509Certificate
 | 
			
		||||
import javax.net.ssl.SSLContext
 | 
			
		||||
import javax.net.ssl.TrustManager
 | 
			
		||||
import javax.net.ssl.X509TrustManager
 | 
			
		||||
 | 
			
		||||
fun getUnsafeHttpClient(): OkHttpClient.Builder =
 | 
			
		||||
    try {
 | 
			
		||||
        // Create a trust manager that does not validate certificate chains
 | 
			
		||||
        val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
 | 
			
		||||
            override fun getAcceptedIssuers(): Array<X509Certificate> =
 | 
			
		||||
                arrayOf()
 | 
			
		||||
 | 
			
		||||
            @Throws(CertificateException::class)
 | 
			
		||||
            override fun checkClientTrusted(
 | 
			
		||||
                chain: Array<java.security.cert.X509Certificate>,
 | 
			
		||||
                authType: String
 | 
			
		||||
            ) {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            @Throws(CertificateException::class)
 | 
			
		||||
            override fun checkServerTrusted(
 | 
			
		||||
                chain: Array<java.security.cert.X509Certificate>,
 | 
			
		||||
                authType: String
 | 
			
		||||
            ) {
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        // Install the all-trusting trust manager
 | 
			
		||||
        val sslContext = SSLContext.getInstance("SSL")
 | 
			
		||||
        sslContext.init(null, trustAllCerts, java.security.SecureRandom())
 | 
			
		||||
 | 
			
		||||
        val sslSocketFactory = sslContext.socketFactory
 | 
			
		||||
 | 
			
		||||
        OkHttpClient.Builder()
 | 
			
		||||
            .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
 | 
			
		||||
            .hostnameVerifier { _, _ -> true }
 | 
			
		||||
    } catch (e: Exception) {
 | 
			
		||||
        throw RuntimeException(e)
 | 
			
		||||
    }
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.text.format.DateUtils
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
import java.text.ParseException
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
fun String.toTextDrawableString(c: Context): String {
 | 
			
		||||
    val textDrawable = StringBuilder()
 | 
			
		||||
    for (s in this.split(" ".toRegex()).filter { !it.isEmpty() }.toTypedArray()) {
 | 
			
		||||
        try {
 | 
			
		||||
            textDrawable.append(s[0])
 | 
			
		||||
        } catch (e: StringIndexOutOfBoundsException) {
 | 
			
		||||
            ACRA.getErrorReporter().maybeHandleSilentException(e, c)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return textDrawable.toString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Item.sourceAndDateText(): String {
 | 
			
		||||
    val formattedDate: String = try {
 | 
			
		||||
        " " + DateUtils.getRelativeTimeSpanString(
 | 
			
		||||
            SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time,
 | 
			
		||||
            Date().time,
 | 
			
		||||
            DateUtils.MINUTE_IN_MILLIS,
 | 
			
		||||
            DateUtils.FORMAT_ABBREV_RELATIVE
 | 
			
		||||
        )
 | 
			
		||||
    } catch (e: ParseException) {
 | 
			
		||||
        e.printStackTrace()
 | 
			
		||||
        ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.sourcetitle + formattedDate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Item.toggleStar(): Item {
 | 
			
		||||
    this.starred = !this.starred
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun List<Item>.flattenTags(): List<Item> =
 | 
			
		||||
    this.flatMap {
 | 
			
		||||
        val item = it
 | 
			
		||||
        val tags: List<String> = it.tags.tags.split(",")
 | 
			
		||||
        tags.map { t ->
 | 
			
		||||
            item.copy(tags = SelfossTagType(t.trim()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -2,80 +2,195 @@ package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.app.PendingIntent
 | 
			
		||||
import android.content.ActivityNotFoundException
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.graphics.BitmapFactory
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.customtabs.CustomTabsIntent
 | 
			
		||||
import android.text.Spannable
 | 
			
		||||
import android.text.style.ClickableSpan
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsIntent
 | 
			
		||||
import android.util.Patterns
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import apps.amine.bou.readerforselfoss.ReaderActivity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
 | 
			
		||||
import xyz.klinker.android.drag_dismiss.DragDismissIntentBuilder
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import org.acra.ACRA
 | 
			
		||||
 | 
			
		||||
fun buildCustomTabsIntent(c: Context): CustomTabsIntent {
 | 
			
		||||
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
 | 
			
		||||
 | 
			
		||||
    fun createPendingShareIntent(c: Context): PendingIntent {
 | 
			
		||||
        val actionIntent = Intent(Intent.ACTION_SEND)
 | 
			
		||||
        actionIntent.type = "text/plain"
 | 
			
		||||
        return PendingIntent.getActivity(
 | 
			
		||||
                c, 0, actionIntent, 0)
 | 
			
		||||
    }
 | 
			
		||||
    val actionIntent = Intent(Intent.ACTION_SEND)
 | 
			
		||||
    actionIntent.type = "text/plain"
 | 
			
		||||
    val createPendingShareIntent: PendingIntent = PendingIntent.getActivity(
 | 
			
		||||
        this,
 | 
			
		||||
        0,
 | 
			
		||||
        actionIntent,
 | 
			
		||||
        0
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val intentBuilder = CustomTabsIntent.Builder()
 | 
			
		||||
 | 
			
		||||
    // TODO: change to primary when it's possible to customize custom tabs title color
 | 
			
		||||
    //intentBuilder.setToolbarColor(c.getResources().getColor(R.color.colorPrimary));
 | 
			
		||||
    intentBuilder.setToolbarColor(c.resources.getColor(R.color.colorAccentDark))
 | 
			
		||||
    intentBuilder.setToolbarColor(resources.getColor(R.color.colorAccentDark))
 | 
			
		||||
    intentBuilder.setShowTitle(true)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    intentBuilder.setStartAnimations(c,
 | 
			
		||||
            R.anim.slide_in_right,
 | 
			
		||||
            R.anim.slide_out_left)
 | 
			
		||||
    intentBuilder.setExitAnimations(c,
 | 
			
		||||
            android.R.anim.slide_in_left,
 | 
			
		||||
            android.R.anim.slide_out_right)
 | 
			
		||||
    intentBuilder.setStartAnimations(
 | 
			
		||||
        this,
 | 
			
		||||
        R.anim.slide_in_right,
 | 
			
		||||
        R.anim.slide_out_left
 | 
			
		||||
    )
 | 
			
		||||
    intentBuilder.setExitAnimations(
 | 
			
		||||
        this,
 | 
			
		||||
        android.R.anim.slide_in_left,
 | 
			
		||||
        android.R.anim.slide_out_right
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val closeicon = BitmapFactory.decodeResource(c.resources, R.drawable.ic_close_white_24dp)
 | 
			
		||||
    val closeicon = BitmapFactory.decodeResource(resources, R.drawable.ic_close_white_24dp)
 | 
			
		||||
    intentBuilder.setCloseButtonIcon(closeicon)
 | 
			
		||||
 | 
			
		||||
    val shareLabel = c.getString(R.string.label_share)
 | 
			
		||||
    val icon = BitmapFactory.decodeResource(c.resources,
 | 
			
		||||
            R.drawable.ic_share_white_24dp)
 | 
			
		||||
    intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent(c))
 | 
			
		||||
    val shareLabel = this.getString(R.string.label_share)
 | 
			
		||||
    val icon = BitmapFactory.decodeResource(
 | 
			
		||||
        resources,
 | 
			
		||||
        R.drawable.ic_share_white_24dp
 | 
			
		||||
    )
 | 
			
		||||
    intentBuilder.setActionButton(icon, shareLabel, createPendingShareIntent)
 | 
			
		||||
 | 
			
		||||
    return intentBuilder.build()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun openItemUrl(i: Item,
 | 
			
		||||
                customTabsIntent: CustomTabsIntent,
 | 
			
		||||
                internalBrowser: Boolean,
 | 
			
		||||
                articleViewer: Boolean,
 | 
			
		||||
                app: Activity,
 | 
			
		||||
                c: Context) {
 | 
			
		||||
    if (!internalBrowser) {
 | 
			
		||||
        val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
        intent.data = Uri.parse(i.getLinkDecoded())
 | 
			
		||||
fun Context.openItemUrlInternally(
 | 
			
		||||
    allItems: ArrayList<Item>,
 | 
			
		||||
    currentItem: Int,
 | 
			
		||||
    linkDecoded: String,
 | 
			
		||||
    customTabsIntent: CustomTabsIntent,
 | 
			
		||||
    articleViewer: Boolean,
 | 
			
		||||
    app: Activity
 | 
			
		||||
) {
 | 
			
		||||
    if (articleViewer) {
 | 
			
		||||
        ReaderActivity.allItems = allItems
 | 
			
		||||
        val intent = Intent(this, ReaderActivity::class.java)
 | 
			
		||||
        intent.putExtra("currentItem", currentItem)
 | 
			
		||||
        app.startActivity(intent)
 | 
			
		||||
    } else {
 | 
			
		||||
        if (articleViewer) {
 | 
			
		||||
            val intent = Intent(c, ReaderActivity::class.java)
 | 
			
		||||
 | 
			
		||||
            DragDismissIntentBuilder(c)
 | 
			
		||||
                    .setFullscreenOnTablets(true)      // defaults to false, tablets will have padding on each side
 | 
			
		||||
                    .setDragElasticity(DragDismissIntentBuilder.DragElasticity.NORMAL)  // Larger elasticities will make it easier to dismiss.
 | 
			
		||||
                    .build(intent)
 | 
			
		||||
 | 
			
		||||
            intent.putExtra("url", i.getLinkDecoded())
 | 
			
		||||
            app.startActivity(intent)
 | 
			
		||||
        } else {
 | 
			
		||||
            CustomTabActivityHelper.openCustomTab(app, customTabsIntent, Uri.parse(i.getLinkDecoded())
 | 
			
		||||
        try {
 | 
			
		||||
            CustomTabActivityHelper.openCustomTab(
 | 
			
		||||
                app,
 | 
			
		||||
                customTabsIntent,
 | 
			
		||||
                Uri.parse(linkDecoded)
 | 
			
		||||
            ) { _, uri ->
 | 
			
		||||
                val intent = Intent(Intent.ACTION_VIEW, uri)
 | 
			
		||||
                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
                c.startActivity(intent)
 | 
			
		||||
                startActivity(intent)
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            openInBrowser(linkDecoded, app)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.openItemUrl(
 | 
			
		||||
    allItems: ArrayList<Item>,
 | 
			
		||||
    currentItem: Int,
 | 
			
		||||
    linkDecoded: String,
 | 
			
		||||
    customTabsIntent: CustomTabsIntent,
 | 
			
		||||
    internalBrowser: Boolean,
 | 
			
		||||
    articleViewer: Boolean,
 | 
			
		||||
    app: Activity
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
    if (!linkDecoded.isUrlValid()) {
 | 
			
		||||
        Toast.makeText(
 | 
			
		||||
            this,
 | 
			
		||||
            this.getString(R.string.cant_open_invalid_url),
 | 
			
		||||
            Toast.LENGTH_LONG
 | 
			
		||||
        ).show()
 | 
			
		||||
    } else {
 | 
			
		||||
        if (!internalBrowser) {
 | 
			
		||||
            openInBrowser(linkDecoded, app)
 | 
			
		||||
        } else {
 | 
			
		||||
            this.openItemUrlInternally(
 | 
			
		||||
                allItems,
 | 
			
		||||
                currentItem,
 | 
			
		||||
                linkDecoded,
 | 
			
		||||
                customTabsIntent,
 | 
			
		||||
                articleViewer,
 | 
			
		||||
                app
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun openInBrowser(linkDecoded: String, app: Activity) {
 | 
			
		||||
    val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
    intent.data = Uri.parse(linkDecoded)
 | 
			
		||||
    try {
 | 
			
		||||
        app.startActivity(intent)
 | 
			
		||||
    } catch (e: ActivityNotFoundException) {
 | 
			
		||||
        Toast.makeText(app.baseContext, e.message, Toast.LENGTH_LONG).show()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun String.isUrlValid(): Boolean =
 | 
			
		||||
    HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
 | 
			
		||||
 | 
			
		||||
fun String.isBaseUrlValid(logErrors: Boolean, ctx: Context): Boolean {
 | 
			
		||||
    val baseUrl = HttpUrl.parse(this)
 | 
			
		||||
    var existsAndEndsWithSlash = false
 | 
			
		||||
    if (baseUrl != null) {
 | 
			
		||||
        val pathSegments = baseUrl.pathSegments()
 | 
			
		||||
        existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun Context.openInBrowserAsNewTask(i: Item) {
 | 
			
		||||
    val intent = Intent(Intent.ACTION_VIEW)
 | 
			
		||||
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
 | 
			
		||||
    intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
 | 
			
		||||
    startActivity(intent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class LinkOnTouchListener: View.OnTouchListener {
 | 
			
		||||
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
 | 
			
		||||
        var ret = false
 | 
			
		||||
        val widget: TextView = v as TextView
 | 
			
		||||
        val text: CharSequence = widget.text
 | 
			
		||||
        val stext = Spannable.Factory.getInstance().newSpannable(text)
 | 
			
		||||
 | 
			
		||||
        val action = event!!.action
 | 
			
		||||
 | 
			
		||||
        if (action == MotionEvent.ACTION_UP ||
 | 
			
		||||
            action == MotionEvent.ACTION_DOWN) {
 | 
			
		||||
            var x: Float = event.x
 | 
			
		||||
            var y: Float = event.y
 | 
			
		||||
 | 
			
		||||
            x -= widget.totalPaddingLeft
 | 
			
		||||
            y -= widget.totalPaddingTop
 | 
			
		||||
 | 
			
		||||
            x += widget.scrollX
 | 
			
		||||
            y += widget.scrollY
 | 
			
		||||
 | 
			
		||||
            val layout = widget.layout
 | 
			
		||||
            val line = layout.getLineForVertical(y.toInt())
 | 
			
		||||
            val off = layout.getOffsetForHorizontal(line, x)
 | 
			
		||||
 | 
			
		||||
            val link = stext.getSpans(off, off, ClickableSpan::class.java)
 | 
			
		||||
 | 
			
		||||
            if (link.isNotEmpty()) {
 | 
			
		||||
                if (action == MotionEvent.ACTION_UP) {
 | 
			
		||||
                    link[0].onClick(widget)
 | 
			
		||||
                }
 | 
			
		||||
                ret = true
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return ret
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils
 | 
			
		||||
 | 
			
		||||
import android.content.res.Resources
 | 
			
		||||
 | 
			
		||||
val Int.toPx: Int
 | 
			
		||||
    get() = (this * Resources.getSystem().displayMetrics.density).toInt()
 | 
			
		||||
 | 
			
		||||
val Int.toDp: Int
 | 
			
		||||
    get() = (this / Resources.getSystem().displayMetrics.density).toInt()
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.bottombar
 | 
			
		||||
 | 
			
		||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
 | 
			
		||||
 | 
			
		||||
fun TextBadgeItem.removeBadge(): TextBadgeItem {
 | 
			
		||||
    this.setText("")
 | 
			
		||||
    this.hide()
 | 
			
		||||
    return this
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun TextBadgeItem.maybeShow(): TextBadgeItem =
 | 
			
		||||
    if (this.isHidden) this.show() else this
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.support.customtabs.CustomTabsClient;
 | 
			
		||||
import android.support.customtabs.CustomTabsIntent;
 | 
			
		||||
import android.support.customtabs.CustomTabsServiceConnection;
 | 
			
		||||
import android.support.customtabs.CustomTabsSession;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsClient;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsIntent;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsSession;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@@ -22,15 +23,15 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens the URL on a Custom Tab if possible. Otherwise fallsback to opening it on a WebView.
 | 
			
		||||
     *
 | 
			
		||||
     * @param activity The host activity.
 | 
			
		||||
     * @param activity         The host activity.
 | 
			
		||||
     * @param customTabsIntent a CustomTabsIntent to be used if Custom Tabs is available.
 | 
			
		||||
     * @param uri the Uri to be opened.
 | 
			
		||||
     * @param fallback a CustomTabFallback to be used if Custom Tabs is not available.
 | 
			
		||||
     * @param uri              the Uri to be opened.
 | 
			
		||||
     * @param fallback         a CustomTabFallback to be used if Custom Tabs is not available.
 | 
			
		||||
     */
 | 
			
		||||
    public static void openCustomTab(Activity activity,
 | 
			
		||||
            CustomTabsIntent customTabsIntent,
 | 
			
		||||
            Uri uri,
 | 
			
		||||
            CustomTabFallback fallback) {
 | 
			
		||||
                                     CustomTabsIntent customTabsIntent,
 | 
			
		||||
                                     Uri uri,
 | 
			
		||||
                                     CustomTabFallback fallback) {
 | 
			
		||||
        String packageName = CustomTabsHelper.getPackageNameToUse(activity);
 | 
			
		||||
 | 
			
		||||
        //If we cant find a package name, it means theres no browser that supports
 | 
			
		||||
@@ -47,6 +48,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Unbinds the Activity from the Custom Tabs Service.
 | 
			
		||||
     *
 | 
			
		||||
     * @param activity the activity that is connected to the service.
 | 
			
		||||
     */
 | 
			
		||||
    public void unbindCustomTabsService(Activity activity) {
 | 
			
		||||
@@ -73,6 +75,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
 | 
			
		||||
     *
 | 
			
		||||
     * @param connectionCallback
 | 
			
		||||
     */
 | 
			
		||||
    public void setConnectionCallback(ConnectionCallback connectionCallback) {
 | 
			
		||||
@@ -81,6 +84,7 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Binds the Activity to the Custom Tabs Service.
 | 
			
		||||
     *
 | 
			
		||||
     * @param activity the activity to be binded to the service.
 | 
			
		||||
     */
 | 
			
		||||
    public void bindCustomTabsService(Activity activity) {
 | 
			
		||||
@@ -94,16 +98,15 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
 | 
			
		||||
     * @return true if call to mayLaunchUrl was accepted.
 | 
			
		||||
     * @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
 | 
			
		||||
     */
 | 
			
		||||
    public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
 | 
			
		||||
        if (mClient == null) return false;
 | 
			
		||||
 | 
			
		||||
        CustomTabsSession session = getSession();
 | 
			
		||||
        if (session == null) return false;
 | 
			
		||||
        return session != null && session.mayLaunchUrl(uri, extras, otherLikelyBundles);
 | 
			
		||||
 | 
			
		||||
        return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -141,9 +144,8 @@ public class CustomTabActivityHelper implements ServiceConnectionCallback {
 | 
			
		||||
     */
 | 
			
		||||
    public interface CustomTabFallback {
 | 
			
		||||
        /**
 | 
			
		||||
         *
 | 
			
		||||
         * @param activity The Activity that wants to open the Uri.
 | 
			
		||||
         * @param uri The uri to be opened by the fallback.
 | 
			
		||||
         * @param uri      The uri to be opened by the fallback.
 | 
			
		||||
         */
 | 
			
		||||
        void openUri(Activity activity, Uri uri);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,21 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.content.pm.ResolveInfo;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.support.customtabs.CustomTabsService;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsService;
 | 
			
		||||
import android.text.TextUtils;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.customtabs.helpers.KeepAliveService;
 | 
			
		||||
 | 
			
		||||
@SuppressWarnings("ALL")
 | 
			
		||||
class CustomTabsHelper {
 | 
			
		||||
    private static final String TAG = "CustomTabsHelper";
 | 
			
		||||
@@ -26,7 +28,8 @@ class CustomTabsHelper {
 | 
			
		||||
 | 
			
		||||
    private static String sPackageNameToUse;
 | 
			
		||||
 | 
			
		||||
    private CustomTabsHelper() {}
 | 
			
		||||
    private CustomTabsHelper() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static void addKeepAliveExtra(Context context, Intent intent) {
 | 
			
		||||
        Intent keepAliveIntent = new Intent().setClassName(
 | 
			
		||||
@@ -38,7 +41,7 @@ class CustomTabsHelper {
 | 
			
		||||
     * Goes through all apps that handle VIEW intents and have a warmup service. Picks
 | 
			
		||||
     * the one chosen by the user if there is one, otherwise makes a best effort to return a
 | 
			
		||||
     * valid package name.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>
 | 
			
		||||
     * This is <strong>not</strong> threadsafe.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context {@link Context} to use for accessing {@link PackageManager}.
 | 
			
		||||
@@ -92,6 +95,7 @@ class CustomTabsHelper {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Used to check whether there is a specialized handler for a given intent.
 | 
			
		||||
     *
 | 
			
		||||
     * @param intent The intent to check with.
 | 
			
		||||
     * @return Whether there is a specialized handler for the given intent.
 | 
			
		||||
     */
 | 
			
		||||
@@ -101,7 +105,7 @@ class CustomTabsHelper {
 | 
			
		||||
            List<ResolveInfo> handlers = pm.queryIntentActivities(
 | 
			
		||||
                    intent,
 | 
			
		||||
                    PackageManager.GET_RESOLVED_FILTER);
 | 
			
		||||
            if (handlers == null || handlers.size() == 0) {
 | 
			
		||||
            if (handlers == null || handlers.isEmpty()) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            for (ResolveInfo resolveInfo : handlers) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.content.ComponentName;
 | 
			
		||||
import android.support.customtabs.CustomTabsClient;
 | 
			
		||||
import android.support.customtabs.CustomTabsServiceConnection;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsClient;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
 | 
			
		||||
 | 
			
		||||
import java.lang.ref.WeakReference;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.customtabs;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.support.customtabs.CustomTabsClient;
 | 
			
		||||
import androidx.browser.customtabs.CustomTabsClient;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
public interface ServiceConnectionCallback {
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the service is connected.
 | 
			
		||||
     *
 | 
			
		||||
     * @param client a CustomTabsClient
 | 
			
		||||
     */
 | 
			
		||||
    void onServiceConnected(CustomTabsClient client);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,15 @@
 | 
			
		||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomBaseViewHolder.java */
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.drawer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
open class CustomBaseViewHolder(var view: View) : RecyclerView.ViewHolder(view) {
 | 
			
		||||
    var icon: ImageView = view.findViewById(R.id.material_drawer_icon) as ImageView
 | 
			
		||||
    var name: TextView = view.findViewById(R.id.material_drawer_name) as TextView
 | 
			
		||||
    var description: TextView = view.findViewById(R.id.material_drawer_description) as TextView
 | 
			
		||||
    var icon: ImageView = view.findViewById(R.id.material_drawer_icon)
 | 
			
		||||
    var name: TextView = view.findViewById(R.id.material_drawer_name)
 | 
			
		||||
    var description: TextView = view.findViewById(R.id.material_drawer_description)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlBasePrimaryDrawerItem.java */
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.drawer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import android.support.annotation.ColorInt
 | 
			
		||||
import android.support.annotation.ColorRes
 | 
			
		||||
import android.support.annotation.StringRes
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import androidx.annotation.ColorInt
 | 
			
		||||
import androidx.annotation.ColorRes
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
 | 
			
		||||
import com.mikepenz.materialdrawer.holder.ColorHolder
 | 
			
		||||
import com.mikepenz.materialdrawer.holder.ImageHolder
 | 
			
		||||
@@ -17,8 +15,8 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
 | 
			
		||||
import com.mikepenz.materialdrawer.util.DrawerUIUtils
 | 
			
		||||
import com.mikepenz.materialize.util.UIUtils
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> : BaseDrawerItem<T, VH>() {
 | 
			
		||||
abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
 | 
			
		||||
    BaseDrawerItem<T, VH>() {
 | 
			
		||||
    fun withIcon(url: String): T {
 | 
			
		||||
        this.icon = ImageHolder(url)
 | 
			
		||||
        return this as T
 | 
			
		||||
@@ -78,7 +76,10 @@ abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
 | 
			
		||||
        val selectedIconColor = getSelectedIconColor(ctx)
 | 
			
		||||
 | 
			
		||||
        //set the background for the item
 | 
			
		||||
        UIUtils.setBackground(viewHolder.view, UIUtils.getSelectableBackground(ctx, selectedColor, true))
 | 
			
		||||
        UIUtils.setBackground(
 | 
			
		||||
            viewHolder.view,
 | 
			
		||||
            UIUtils.getSelectableBackground(ctx, selectedColor, true)
 | 
			
		||||
        )
 | 
			
		||||
        //set the text for the name
 | 
			
		||||
        StringHolder.applyTo(this.getName(), viewHolder.name)
 | 
			
		||||
        //set the text for the description or hide
 | 
			
		||||
@@ -87,8 +88,11 @@ abstract class CustomUrlBasePrimaryDrawerItem<T, VH : RecyclerView.ViewHolder> :
 | 
			
		||||
        //set the colors for textViews
 | 
			
		||||
        viewHolder.name.setTextColor(getTextColorStateList(color, selectedTextColor))
 | 
			
		||||
        //set the description text color
 | 
			
		||||
        ColorHolder.applyToOr(descriptionTextColor,
 | 
			
		||||
                viewHolder.description, getTextColorStateList(color, selectedTextColor))
 | 
			
		||||
        ColorHolder.applyToOr(
 | 
			
		||||
            descriptionTextColor,
 | 
			
		||||
            viewHolder.description,
 | 
			
		||||
            getTextColorStateList(color, selectedTextColor)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        //define the typeface for our textViews
 | 
			
		||||
        if (getTypeface() != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,18 @@
 | 
			
		||||
/* From https://github.com/mikepenz/MaterialDrawer/blob/develop/app/src/main/java/com/mikepenz/materialdrawer/app/drawerItems/CustomUrlPrimaryDrawerItem.java */
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.drawer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.support.annotation.LayoutRes
 | 
			
		||||
import android.support.annotation.StringRes
 | 
			
		||||
import androidx.annotation.LayoutRes
 | 
			
		||||
import androidx.annotation.StringRes
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import com.mikepenz.materialdrawer.holder.BadgeStyle
 | 
			
		||||
import com.mikepenz.materialdrawer.holder.StringHolder
 | 
			
		||||
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(), ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
 | 
			
		||||
class CustomUrlPrimaryDrawerItem :
 | 
			
		||||
    CustomUrlBasePrimaryDrawerItem<CustomUrlPrimaryDrawerItem, CustomUrlPrimaryDrawerItem.ViewHolder>(),
 | 
			
		||||
    ColorfulBadgeable<CustomUrlPrimaryDrawerItem> {
 | 
			
		||||
    protected var mBadge: StringHolder = StringHolder("")
 | 
			
		||||
    protected var mBadgeStyle = BadgeStyle()
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +65,10 @@ class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrima
 | 
			
		||||
        val badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge)
 | 
			
		||||
        //style the badge if it is visible
 | 
			
		||||
        if (badgeVisible) {
 | 
			
		||||
            mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx)))
 | 
			
		||||
            mBadgeStyle.style(
 | 
			
		||||
                viewHolder.badge,
 | 
			
		||||
                getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx))
 | 
			
		||||
            )
 | 
			
		||||
            viewHolder.badgeContainer.visibility = View.VISIBLE
 | 
			
		||||
        } else {
 | 
			
		||||
            viewHolder.badgeContainer.visibility = View.GONE
 | 
			
		||||
@@ -88,7 +89,6 @@ class CustomUrlPrimaryDrawerItem : CustomUrlBasePrimaryDrawerItem<CustomUrlPrima
 | 
			
		||||
 | 
			
		||||
    class ViewHolder(view: View) : CustomBaseViewHolder(view) {
 | 
			
		||||
        val badgeContainer: View = view.findViewById(R.id.material_drawer_badge_container)
 | 
			
		||||
        val badge: TextView = view.findViewById(R.id.material_drawer_badge) as TextView
 | 
			
		||||
 | 
			
		||||
        val badge: TextView = view.findViewById(R.id.material_drawer_badge)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.glide
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Bitmap
 | 
			
		||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.request.RequestOptions
 | 
			
		||||
import com.bumptech.glide.request.target.BitmapImageViewTarget
 | 
			
		||||
 | 
			
		||||
fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
 | 
			
		||||
    Glide.with(this)
 | 
			
		||||
        .asBitmap()
 | 
			
		||||
        .load(url)
 | 
			
		||||
        .apply(RequestOptions.centerCropTransform())
 | 
			
		||||
        .into(iv)
 | 
			
		||||
 | 
			
		||||
fun Context.bitmapFitCenter(url: String, iv: ImageView) =
 | 
			
		||||
    Glide.with(this)
 | 
			
		||||
        .asBitmap()
 | 
			
		||||
        .load(url)
 | 
			
		||||
        .apply(RequestOptions.fitCenterTransform())
 | 
			
		||||
        .into(iv)
 | 
			
		||||
 | 
			
		||||
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
 | 
			
		||||
    Glide.with(this)
 | 
			
		||||
        .asBitmap()
 | 
			
		||||
        .load(url)
 | 
			
		||||
        .apply(RequestOptions.centerCropTransform())
 | 
			
		||||
        .into(object : BitmapImageViewTarget(iv) {
 | 
			
		||||
            override fun setResource(resource: Bitmap?) {
 | 
			
		||||
                val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(
 | 
			
		||||
                    resources,
 | 
			
		||||
                    resource
 | 
			
		||||
                )
 | 
			
		||||
                circularBitmapDrawable.isCircular = true
 | 
			
		||||
                iv.setImageDrawable(circularBitmapDrawable)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.glide
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.Config
 | 
			
		||||
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import com.bumptech.glide.GlideBuilder
 | 
			
		||||
import com.bumptech.glide.Registry
 | 
			
		||||
import com.bumptech.glide.load.model.GlideUrl
 | 
			
		||||
import com.bumptech.glide.module.GlideModule
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
 | 
			
		||||
class SelfSignedGlideModule : GlideModule {
 | 
			
		||||
 | 
			
		||||
    override fun applyOptions(context: Context?, builder: GlideBuilder?) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
 | 
			
		||||
 | 
			
		||||
        if (context != null) {
 | 
			
		||||
            val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
 | 
			
		||||
            if (pref.getBoolean("isSelfSignedCert", false)) {
 | 
			
		||||
                val client = getUnsafeHttpClient().build()
 | 
			
		||||
 | 
			
		||||
                registry?.append(
 | 
			
		||||
                    GlideUrl::class.java,
 | 
			
		||||
                    InputStream::class.java,
 | 
			
		||||
                    com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader.Factory(client)
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.network
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Color
 | 
			
		||||
import android.net.ConnectivityManager
 | 
			
		||||
import android.net.NetworkInfo
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.TextView
 | 
			
		||||
import apps.amine.bou.readerforselfoss.R
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
 | 
			
		||||
var snackBarShown = false
 | 
			
		||||
var view: View? = null
 | 
			
		||||
lateinit var s: Snackbar
 | 
			
		||||
 | 
			
		||||
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
 | 
			
		||||
    val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
 | 
			
		||||
    val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
 | 
			
		||||
    val networkIsAccessible = activeNetwork != null && activeNetwork.isConnectedOrConnecting
 | 
			
		||||
 | 
			
		||||
    if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
 | 
			
		||||
        view = v
 | 
			
		||||
        s = Snackbar
 | 
			
		||||
            .make(
 | 
			
		||||
                v,
 | 
			
		||||
                R.string.no_network_connectivity,
 | 
			
		||||
                Snackbar.LENGTH_INDEFINITE
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        s.setAction(android.R.string.ok) {
 | 
			
		||||
            snackBarShown = false
 | 
			
		||||
            s.dismiss()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        val view = s.view
 | 
			
		||||
        val tv: TextView = view.findViewById(com.google.android.material.R.id.snackbar_text)
 | 
			
		||||
        tv.setTextColor(Color.WHITE)
 | 
			
		||||
        s.show()
 | 
			
		||||
        snackBarShown = true
 | 
			
		||||
    }
 | 
			
		||||
    if (snackBarShown && networkIsAccessible && !overrideOffline) {
 | 
			
		||||
        s.dismiss()
 | 
			
		||||
    }
 | 
			
		||||
    return if(overrideOffline) overrideOffline else networkIsAccessible
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,73 @@
 | 
			
		||||
package apps.amine.bou.readerforselfoss.utils.persistence
 | 
			
		||||
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Item
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossTagType
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Source
 | 
			
		||||
import apps.amine.bou.readerforselfoss.api.selfoss.Tag
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.ItemEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.SourceEntity
 | 
			
		||||
import apps.amine.bou.readerforselfoss.persistence.entities.TagEntity
 | 
			
		||||
 | 
			
		||||
fun TagEntity.toView(): Tag =
 | 
			
		||||
        Tag(
 | 
			
		||||
            this.tag,
 | 
			
		||||
            this.color,
 | 
			
		||||
            this.unread
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
fun SourceEntity.toView(): Source =
 | 
			
		||||
        Source(
 | 
			
		||||
            this.id,
 | 
			
		||||
            this.title,
 | 
			
		||||
            SelfossTagType(this.tags),
 | 
			
		||||
            this.spout,
 | 
			
		||||
            this.error,
 | 
			
		||||
            this.icon
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
fun Source.toEntity(): SourceEntity =
 | 
			
		||||
        SourceEntity(
 | 
			
		||||
            this.id,
 | 
			
		||||
            this.title,
 | 
			
		||||
            this.tags.tags,
 | 
			
		||||
            this.spout,
 | 
			
		||||
            this.error,
 | 
			
		||||
            this.icon.orEmpty()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
fun Tag.toEntity(): TagEntity =
 | 
			
		||||
        TagEntity(
 | 
			
		||||
            this.tag,
 | 
			
		||||
            this.color,
 | 
			
		||||
            this.unread
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
fun ItemEntity.toView(): Item =
 | 
			
		||||
        Item(
 | 
			
		||||
            this.id,
 | 
			
		||||
            this.datetime,
 | 
			
		||||
            this.title,
 | 
			
		||||
            this.content,
 | 
			
		||||
            this.unread,
 | 
			
		||||
            this.starred,
 | 
			
		||||
            this.thumbnail,
 | 
			
		||||
            this.icon,
 | 
			
		||||
            this.link,
 | 
			
		||||
            this.sourcetitle,
 | 
			
		||||
            SelfossTagType(this.tags)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
fun Item.toEntity(): ItemEntity =
 | 
			
		||||
    ItemEntity(
 | 
			
		||||
        this.id,
 | 
			
		||||
        this.datetime,
 | 
			
		||||
        this.title,
 | 
			
		||||
        this.content,
 | 
			
		||||
        this.unread,
 | 
			
		||||
        this.starred,
 | 
			
		||||
        this.thumbnail,
 | 
			
		||||
        this.icon,
 | 
			
		||||
        this.link,
 | 
			
		||||
        this.sourcetitle,
 | 
			
		||||
        this.tags.tags
 | 
			
		||||
    )
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:viewportWidth="24.0"
 | 
			
		||||
        android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FFFFFFFF"
 | 
			
		||||
        android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:viewportWidth="24.0"
 | 
			
		||||
        android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FFFFFFFF"
 | 
			
		||||
        android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:viewportWidth="24.0"
 | 
			
		||||
        android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FF000000"
 | 
			
		||||
        android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
        android:width="24dp"
 | 
			
		||||
        android:height="24dp"
 | 
			
		||||
        android:viewportWidth="24.0"
 | 
			
		||||
        android:viewportHeight="24.0">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="#FF000000"
 | 
			
		||||
        android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_action_lab.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 683 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 134 B  | 
| 
		 Before Width: | Height: | Size: 124 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_bug_report.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 271 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_chrome_reader_mode.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 216 B  | 
| 
		 After Width: | Height: | Size: 206 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_cloud_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 334 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_color_lens_black_24dp.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 458 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_fiber_new.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 324 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_history.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 551 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_info_outline.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 551 B  | 
| 
		 Before Width: | Height: | Size: 953 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_open_in_browser.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 204 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-hdpi/ic_settings.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 498 B  | 
| 
		 After Width: | Height: | Size: 523 B  | 
| 
		 Before Width: | Height: | Size: 434 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_action_lab.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 409 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_add.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 116 B  | 
| 
		 Before Width: | Height: | Size: 86 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_bug_report.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 212 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_chrome_reader_mode.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 136 B  | 
| 
		 After Width: | Height: | Size: 134 B  | 
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-mdpi/ic_cloud_download.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 228 B  |