Compare commits

...

94 Commits

Author SHA1 Message Date
4a9151e4aa Release 0.6.4 2017-11-23 18:38:51 +01:00
020cc89576 Translations (#970)
* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (French)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/fr/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Added translation using Weblate (Indonesian)

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Arabic)

Currently translated at 0.2% (1 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 0.5% (2 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.1% (4 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.3% (5 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.6% (6 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.9% (7 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 2.2% (8 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.0% (11 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.3% (12 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.9% (14 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 4.4% (16 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 6.1% (22 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 6.7% (24 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 7.2% (26 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 8.3% (30 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 8.9% (32 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 10.6% (38 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 10.8% (39 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.0% (43 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.2% (44 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.5% (45 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 15.9% (57 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 16.4% (59 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 16.7% (60 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.3% (62 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.3% (62 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.8% (64 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 18.1% (65 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 19.2% (69 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 21.2% (76 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 22.0% (79 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 22.3% (80 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 23.4% (84 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 25.1% (90 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 25.4% (91 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 26.5% (95 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 26.8% (96 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 27.6% (99 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 27.9% (100 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 28.2% (101 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 28.4% (102 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.0% (104 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.3% (105 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.8% (107 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.1% (108 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.1% (108 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.4% (109 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 31.2% (112 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 31.8% (114 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 32.9% (118 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 33.2% (119 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 33.7% (121 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 34.0% (122 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 35.1% (126 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 35.4% (127 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 36.0% (129 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 36.3% (130 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 55.3% (198 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Polish)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (German)

Currently translated at 99.4% (368 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 99.4% (368 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (Polish)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pl/

* Translated using Weblate (Dutch)

Currently translated at 96.5% (359 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 96.7% (360 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 97.3% (362 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 97.5% (363 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (French)

Currently translated at 97.8% (364 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (French)

Currently translated at 97.8% (364 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (French)

Currently translated at 98.9% (368 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (Dutch)

Currently translated at 99.4% (370 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (French)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Added translation using Weblate (Hungarian)

* Translated using Weblate (Hungarian)

Currently translated at 25.5% (95 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 25.8% (96 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Russian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ru/

* Translated using Weblate (Hungarian)

Currently translated at 30.3% (113 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 30.3% (113 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.0% (123 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.0% (123 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.8% (126 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.8% (126 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.0% (138 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.3% (139 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.9% (141 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 38.1% (142 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Indonesian)

Currently translated at 98.3% (366 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 99.1% (369 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 99.1% (369 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (German)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/de/

* Added translation using Weblate (Malay)

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (German)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/de/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Malay)

Currently translated at 62.9% (234 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (Malay)

Currently translated at 83.8% (312 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 84.1% (313 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/
2017-11-23 18:24:59 +01:00
25898d34ca Update page indicator colors and new Readmangatoday domain 2017-11-23 18:00:49 +01:00
6394388714 Fix #764. Update Kissmanga genres 2017-11-22 21:41:57 +01:00
d4101c7bdf Page indicator now uses an outline instead of overlapping shadows 2017-11-20 13:55:56 +01:00
c93bf89cbe Fix crash downloading of manga from Readmangatoday (#1071)
Close #1070
2017-11-20 10:06:23 +01:00
88d1f29fe2 Move page indicator to bottom center, and use a shadow instead of a background. Other category in catalogue list is now placed at the end 2017-11-18 14:09:38 +01:00
c437a33f2a A few fixes and dependency updates 2017-11-11 15:31:32 +01:00
e3259f39f1 Fallback covers' external directory 2017-11-08 22:34:56 +01:00
cb357b0a16 Fix some crashes 2017-11-08 22:25:00 +01:00
a7faf445c4 Add concurrency to global search queries 2017-11-08 21:02:19 +01:00
82a08f24c0 added Cloudflare email obfuscation bypass for kissmanga (#1054)
* added Cloudflare email obfuscation bypass for kissmanga

* added ignore case.
2017-11-06 22:20:04 +01:00
afa89ac125 added extra padding on page number to prevent cut off on rounded corner devices. (#1056) 2017-11-06 22:08:49 +01:00
2060b5cd34 Fix extra (advertisement) page being added in Mangahere. (#1052) 2017-11-05 10:42:02 +01:00
d69730a333 Undo last commit 2017-11-05 00:47:31 +01:00
9714a30148 Share image with both setData and extra stream intent 2017-11-04 18:05:02 +01:00
23c0f2c313 Update subsampling. Export /storage/ to SAF 2017-11-02 18:49:09 +01:00
5c4139be45 Update flexible adapter. Show fast scroller in chapters screen 2017-11-02 16:58:32 +01:00
6b1a3a20e5 Fix covers url on Mangachan. (#1045)
Fix some warnings.
2017-10-29 15:22:04 +01:00
4ae00c80ca Fix many compilation warnings 2017-10-28 19:12:17 +02:00
827792c4f0 Restore previous query in global search. Closes #1040 2017-10-28 18:26:31 +02:00
abbe700dac Build command and paths also changed 2017-10-28 17:01:17 +02:00
1347bfe243 This one should finally fix travis builds 2017-10-28 16:39:34 +02:00
a76ee95b6d Trying to fix broken travis 2017-10-28 16:24:18 +02:00
f3689f09cd Use gradle's new dependencies API. Update a few dependencies 2017-10-28 16:10:51 +02:00
d545cfd38c Update Android Studio to 3.0 2017-10-28 14:44:19 +02:00
3631a9fac2 Use new key format in badges preference 2017-10-24 17:27:45 +02:00
aee4ad2d3f Fix test 2017-10-22 18:58:31 +02:00
f88c86c799 Download count shouldn't be stored as a database field 2017-10-21 23:43:46 +02:00
60ac27e401 Add library manga class 2017-10-21 20:13:41 +02:00
d0567de4e6 Download badge 2017-10-21 17:08:49 +02:00
ca30fd6088 Actually use latest Glide version. Minor doc fix 2017-10-14 18:37:23 +02:00
len
1470e9d5ca Glide v4 2017-10-14 18:16:11 +02:00
len
f45efe2aa8 Library updater is now a foreground service 2017-10-14 13:05:02 +02:00
5b6c475817 fixed author/artist not showing for Mangahere. (#1032) 2017-10-13 00:18:17 +02:00
len
4abd2d709f Introduce coroutines. Fix #1027. Lower notification channels' importance 2017-10-13 00:12:29 +02:00
d97aff85b3 Support notification channels. Fixes #995 2017-10-10 14:16:37 +02:00
deec65446f Shortcut fix Oreo (#1026)
* Moved to Android O Shortcutmanager

* Re-added possibility to change icon shape pre oreo.
2017-10-10 12:05:33 +02:00
len
5c662b1ae1 Fix app freezes when queueing many chapters with SAF. Closes #817 2017-10-05 10:15:02 +02:00
f648940388 Fix wrong downloaded percentage when server doesn't send content length. Fixes #1019 2017-10-05 08:41:28 +02:00
len
5aae17754f Travis: forgot to add semicolons 2017-10-01 12:15:06 +02:00
len
0ef0f6ece1 Travis: don't decode secrets for PRs 2017-10-01 12:05:38 +02:00
bfd46f28e0 Reversed some things from AMOLED update (#1015) 2017-10-01 10:44:48 +02:00
len
eaece18afc Travis: use default tools 2017-10-01 00:14:11 +02:00
len
67d39b037c Clone NDK for Travis 2017-09-30 22:15:15 +02:00
len
dd3f5a146d Fix commit count in travis' build 2017-09-30 20:40:44 +02:00
len
9fdc5b4b9d Remove teamcity badge. Fix deploy script 2017-09-30 19:05:41 +02:00
1f32d13698 Travis deployment (#1014)
* Travis deployment

* Executable scripts
2017-09-30 18:49:43 +02:00
len
886b1019ed Update android sdk in travis 2017-09-29 20:24:21 +02:00
len
3e8ed8a171 Also set system locale with the Java API. Closes #978 2017-09-29 20:08:40 +02:00
len
8307daee63 Trust mangahere certificate for now... 2017-09-29 18:31:18 +02:00
9b40d10352 Improved AMOLED theme. Added Button style for borderless buttons. (#1009)
* Improved AMOLED theme. Added Button style for borderless buttons. Some UI improvements.

* Deleted unused drawables from app.
2017-09-29 08:34:13 +02:00
f2a06eab37 Fix downloads from mangahere (#997) 2017-09-26 22:15:27 +02:00
74fd70416f Add option to sort library by source. (#985)
* Add option to sort library by source.

* Implement change suggested by NoodleMage.
2017-09-24 20:01:07 +02:00
b85c164195 added licensed element check for MangaFox (#977) 2017-09-24 10:54:39 +02:00
len
75c41b645a Target sdk 26. Dependency updates. 2017-09-23 17:14:04 +02:00
54c8b3ef29 Global Search (#849)
* Global Search

* Cards are now independent of design by use of recycler.

* Added local

* Some attribute fixes + moved onclick to controller.

* Lots of improvements to code

* Reversed some stuff. Thanks API 16

* Code fixes

* Performance improvements

* Moved adapter creation to constructor

* Small changes

* Removed sources settings from settings menu. Added OnChangeListener in catalogue. Made setting icon visible if room.

* bug fix

* Code review part uno

* Code review part uno-2

* Single recycler approach

* Add last source used

* Fix scroll state and some layout issues

* Fix wrong item binding

* Use data class for items

* Calculate item position and count while binding

* Fix background color with slices

* Reuse slices. Fix card background. Flatten constraint layout

* Fix global_search scroll issue

* Store last state with global search

* Minor changes

* Remove catalogue toolbar spinner. Persist catalogue across process restarts

* Save view state of recycler views. Set toolbar title with current query
2017-09-23 13:11:39 +02:00
56bde40035 Fix search on readmanga/mintmanga (#986) 2017-09-19 20:25:09 +02:00
ff4a015baa Added regex to strip local manga chapter names (#983)
* added regex to strip non-alphanumeric characters in local filenames
2017-09-18 11:20:34 +02:00
len
0db4fcc27e Release 0.6.3 2017-09-10 12:29:17 +02:00
len
f3080b6277 Actually convert file uri to content uri 2017-09-10 12:29:04 +02:00
len
69cbbd5811 Disable file exposure detection to allow sharing from the local cache ¯\_(ツ)_/¯ 2017-09-10 12:03:44 +02:00
len
0b85760939 Fix #908 2017-09-10 10:51:21 +02:00
len
03f3a4805f Fix a crash when retrying a page 2017-09-10 10:14:40 +02:00
len
d95adf2631 Release 0.6.2 2017-09-09 14:00:21 +02:00
len
e971d40e06 Lock drawer with gravity parameter instead of a view 2017-09-08 18:18:27 +02:00
len
c65a01a5f0 Fix a crash when retrying pages 2017-09-08 17:59:43 +02:00
len
8586014e17 Exclude extensions dependencies from proguard. Enable new translations. 2017-09-08 17:43:46 +02:00
bdfae4ba04 Translations (#881)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Add translation link

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (Korean)

Currently translated at 25.1% (90 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ko/

* Translated using Weblate (Russian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ru/

* Added translation using Weblate (German)

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/
2017-09-08 17:25:44 +02:00
75cb94b51a Fix tint on AMOLED theme (#966)
Fix tint on AMOLED theme
2017-08-30 21:50:19 +02:00
2f6d163a7a Simplify presenter delegate 2017-08-29 10:39:22 +02:00
ecfe72bcad Let GC take care of the presenter. Also fix #947 2017-08-29 09:55:42 +02:00
e6ff9e18cc Fix #956 2017-08-28 10:13:27 +02:00
3c550c1781 Kotlin 1.1.4. Add discord link in about 2017-08-28 10:00:13 +02:00
537693f5cf README, CONTRIBUTING and ISSUE_TEMPLATE, Discord (#952)
* Include discord, change issue segment

* Update CONTRIBUTING.md

* Update ISSUE_TEMPLATE.md
2017-08-28 09:42:44 +02:00
5ae0589547 License manga update and Manga Fox Title Update (#937)
* update mangafox parsing to read chapter title also if it exists.

* updated chapterImpl to force update chapters if chapter name changes.  This allows for chapter name changes from the source to update in app

* switched from : to - since other sites that already have title use - so it provides consistency across sources.

* fixed spacing for -

* fixes license status for manga fox
if manga is licensed no chapters will be shown.

* 1. changed equality in chapterImp back to just the url (removed scanlator, and name comparison)
2. Removed extra line of code assigning mangaFox title twice
3. Modified ChapterSourceSync for scanlator/title/url comparison.

* cleaned spaces, added comment, incorporated toChange code from other pull request

* throw exception instead of returning empty list when licensed

* space fix
2017-08-28 09:10:19 +02:00
len
71fc6fc257 Revert chapter equals method 2017-08-26 12:50:52 +02:00
len
c0d7b16ee6 Allow to update chapter metadata 2017-08-26 12:46:35 +02:00
len
f3f7aa9e1d Also fix Batoto popular query 2017-08-25 23:30:18 +02:00
len
43355970db Batoto fix. #953 2017-08-25 23:10:31 +02:00
bfa386acba Add Filter by Completed for library (#941)
* issue 938

added filter by completed manga status

* changed to use existing string
fixed space issue in method in presenter
2017-08-19 20:34:43 +02:00
len
e8b432485d Minor changes 2017-08-15 15:06:21 +02:00
a12a34e3bb Add Batoto Scanlator to Chapter view (#789)
* Added scanlator for Batoto on chapter list

* adjusted item_chapter layout for scanlator
adjusted so db chapters get updated if scanlator does not match source scanlator

* adjusted item_chapter layout for scanlator
adjusted chapter holder to dynamically set title max lines depending on if scanlator exists

* fixed excess blank line

* changed scanlator to be instantiated instead lateint to prevent toast message erro when viewing chapters by catalog

* changed item_chapter.xml to constraint layout

* removed accidental changes to catalog

* cleaned up code.

* fixed issue where long title was running into 3 dot menu
fixed issue where no scanlator for manga was causing date to not be bottom lined
fixed general chapter layout to be more similar to existing

* allow scanlator to be null

* fixed merge issue

* fixed merge issue

* attempt to fix whitespace carriage return issue

* attempt to fix whitespace carriage return issue

* attempt to fix whitespace carriage return issue
2017-08-15 15:05:41 +02:00
b79855c01d Remove circle image view dependency 2017-08-09 12:38:54 +02:00
17fe501a6d Ask permissions once. Fixes #892 2017-08-07 11:04:27 +02:00
len
8201b367ec Fix most crashes with extensions and the release version. Crop borders support in android O 2017-08-06 16:19:25 +02:00
len
6c242084ca Fallback chapter cache to internal storage 2017-08-03 21:44:31 +02:00
aefe7b176a Fixes case where manga name ends with s. (#919) 2017-07-31 20:04:14 +02:00
6059b85e58 Fix library category not updatable when empty. Closes #907 2017-07-27 09:21:15 +02:00
len
aa46c52eee Crop borders for webtoons. Closes #904 2017-07-26 20:35:31 +02:00
d3cbfbdb59 Add workaround for disappearing menu items 2017-07-26 11:29:03 +02:00
cc9b77b876 Simultaneous download will now show on start. (#911) 2017-07-26 10:49:13 +02:00
len
1568ac9e8a Release 0.6.1 2017-07-08 19:01:49 +02:00
len
1129dacdfa Downgrade jsoup 2017-07-08 18:58:53 +02:00
225 changed files with 6271 additions and 1907 deletions

View File

@ -1,6 +1,17 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [Feature requests](#feature-requests)
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
***
# Catalogue requests # Catalogue requests
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here * Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs # Bugs
* Include version (Setting > About > Version) * Include version (Setting > About > Version)
@ -8,17 +19,9 @@
* Dev version is equal to the number of commits as seen in the main page * Dev version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed) * If it could be device-dependent, try reproducing on another device (if possible)
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
* For large logs use http://pastebin.com/ (or similar) * For large logs use http://pastebin.com/ (or similar)
* For multipart issues **use list** like this: * Don't group unrelated requests into one issue
* [x] Done
* [ ] Not done
```
* [x] Done
* [ ] Not done
```
* Don't put together too many unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
@ -28,7 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does" * Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed) * Include screenshot (if needed)
# Translations
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)

View File

@ -1,7 +1 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting** **Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
Remove line above and describe your issue here. Fill out version below. Use Preview.
Version: r000 or v0.0.0
(other relevant info like OS)

View File

@ -1,41 +1,57 @@
language: android language: android
android: android:
components: components:
- platform-tools - build-tools-26.0.2
- tools - android-26
- extra-android-m2repository
# The BuildTools version used by your project - extra-google-m2repository
- build-tools-25.0.1 - extra-android-support
- android-25 - extra-google-google_play_services
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
licenses: licenses:
- android-sdk-license-.+ - android-sdk-license-.+
- '.+'
jdk:
- oraclejdk8
before_script:
- chmod +x gradlew
before_install: before_install:
- mkdir "$ANDROID_HOME/licenses" || true - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" tar xf secrets.tar;
mv debug.keystore "$HOME/.android";
#Build, and run tests fi
script: "./gradlew clean buildStandardDebug" - git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root
sudo: false - export ANDROID_NDK_HOME=$HOME/android-ndk-root
- mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
script: ".travis/build.sh"
before_cache: before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache: cache:
directories: directories:
- $HOME/.gradle/caches/ - "$HOME/.gradle/caches/"
- $HOME/.gradle/wrapper/ - "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache"
deploy:
- provider: releases
api_key:
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
file: tachiyomi-v*.apk
file_glob: true
skip_cleanup: true
on:
tags: true
repo: inorichi/tachiyomi
- provider: script
script: ".travis/deploy.sh"
skip_cleanup: true
on:
branch: master
condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi
env: env:
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m" global:
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=

20
.travis/build.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
git fetch --unshallow #required for commit count
if [ -z "$TRAVIS_TAG" ]; then
./gradlew clean assembleStandardDebug
COMMIT_COUNT=$(git rev-list --count HEAD)
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
else
./gradlew clean assembleStandardRelease
TOOLS="${ANDROID_HOME}/build-tools/26.0.1"
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
fi

15
.travis/deploy.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
pattern="tachiyomi-r*"
files=( $pattern )
export ARTIFACT="${files[0]}"
if [ -z "$ARTIFACT" ]; then
echo "Artifact not found"
exit 1
fi
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest

BIN
.travis/secrets.tar.enc Normal file

Binary file not shown.

View File

@ -1,10 +1,11 @@
| Build | Download | F-Droid | | Build | Download | F-Droid | Contribute | Contact |
|-------|----------|-------------| |-------|----------|---------|------------|---------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | | [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4) |
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md) ### **Contact us on [Discord](https://discord.gg/WrBkRk4)**
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.** ***
Tachiyomi is a free and open source manga reader for Android. Tachiyomi is a free and open source manga reader for Android.

View File

@ -3,10 +3,10 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.zellius.shortcut-helper'
if (file("custom.gradle").exists()) { shortcutHelper.filePath = './shortcuts.xml'
apply from: "custom.gradle"
}
ext { ext {
// Git is needed in your system PATH for these commands to work. // Git is needed in your system PATH for these commands to work.
@ -29,17 +29,17 @@ ext {
} }
android { android {
compileSdkVersion 25 compileSdkVersion 26
buildToolsVersion "25.0.2" buildToolsVersion "26.0.2"
publishNonDefault true publishNonDefault true
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi" applicationId "eu.kanade.tachiyomi"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 25 targetSdkVersion 26
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 23 versionCode 27
versionName "0.6.0" versionName "0.6.4"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -67,15 +67,20 @@ android {
} }
} }
flavorDimensions "default"
productFlavors { productFlavors {
standard { standard {
buildConfigField "boolean", "INCLUDE_UPDATER", "true" buildConfigField "boolean", "INCLUDE_UPDATER", "true"
dimension "default"
} }
fdroid { fdroid {
dimension "default"
} }
dev { dev {
minSdkVersion 21 minSdkVersion 21
resConfigs "en", "xxhdpi" resConfigs "en", "xxhdpi"
dimension "default"
} }
} }
@ -97,127 +102,137 @@ android {
dependencies { dependencies {
// Modified dependencies // Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385' implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883'
compile 'com.github.inorichi:junrar-android:634c1f5' implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library // Android support library
final support_library_version = '25.4.0' final support_library_version = '26.1.0'
compile "com.android.support:support-v4:$support_library_version" implementation "com.android.support:support-v4:$support_library_version"
compile "com.android.support:appcompat-v7:$support_library_version" implementation "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version" implementation "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:design:$support_library_version" implementation "com.android.support:design:$support_library_version"
compile "com.android.support:recyclerview-v7:$support_library_version" implementation "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:support-annotations:$support_library_version" implementation "com.android.support:preference-v7:$support_library_version"
compile "com.android.support:customtabs:$support_library_version" implementation "com.android.support:support-annotations:$support_library_version"
implementation "com.android.support:customtabs:$support_library_version"
compile 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:multidex:1.0.1' implementation 'com.android.support:multidex:1.0.1'
// ReactiveX // ReactiveX
compile 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.3.0' implementation 'io.reactivex:rxjava:1.3.3'
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
compile 'com.github.pwittchen:reactivenetwork:0.7.0' implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client // Network client
compile "com.squareup.okhttp3:okhttp:3.8.1" implementation "com.squareup.okhttp3:okhttp:3.9.0"
compile 'com.squareup.okio:okio:1.13.0' implementation 'com.squareup.okio:okio:1.13.0'
// REST // REST
final retrofit_version = '2.3.0' final retrofit_version = '2.3.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON // JSON
compile 'com.google.code.gson:gson:2.8.1' implementation 'com.google.code.gson:gson:2.8.2'
compile 'com.github.salomonbrys.kotson:kotson:2.5.0' implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// YAML // YAML
compile 'com.github.bmoliveira:snake-yaml:v1.18-android' implementation 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine // JavaScript engine
compile 'com.squareup.duktape:duktape-android:1.1.0' implementation 'com.squareup.duktape:duktape-android:1.2.0'
// Disk // Disk
compile 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
compile 'com.github.seven332:unifile:1.0.0' implementation 'com.github.seven332:unifile:1.0.0'
// HTML parser // HTML parser
compile 'org.jsoup:jsoup:1.10.3' implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling // Job scheduling
compile 'com.evernote:android-job:1.1.11' implementation 'com.evernote:android-job:1.2.0'
compile 'com.google.android.gms:play-services-gcm:11.0.1' implementation 'com.google.android.gms:play-services-gcm:11.6.0'
// Changelog // Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database // Database
compile "com.pushtorefresh.storio:sqlite:1.13.0" implementation "com.pushtorefresh.storio:sqlite:1.13.0"
// Model View Presenter // Model View Presenter
final nucleus_version = '3.0.0' final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version" implementation "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version" implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection // Dependency injection
compile "uy.kohesive.injekt:injekt-core:1.16.1" implementation "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library // Image library
compile 'com.github.bumptech.glide:glide:3.8.0' final glide_version = '4.3.1'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar' implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Transformations // Transformations
compile 'jp.wasabeef:glide-transformations:2.0.2' implementation 'jp.wasabeef:glide-transformations:3.0.1'
// Logging // Logging
compile 'com.jakewharton.timber:timber:4.5.1' implementation 'com.jakewharton.timber:timber:4.6.0'
// Crash reports // Crash reports
compile 'ch.acra:acra:4.9.2' implementation 'ch.acra:acra:4.9.2'
// Sort // Sort
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI // UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:5.0.0-rc1' implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
compile 'com.nononsenseapps:filepicker:2.5.2' implementation 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e' implementation 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.9.4.5' implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
compile 'me.zhanghai.android.systemuihelper:library:1.0.0' exclude group: "com.android.support", module: "support-v13"
compile 'de.hdodenhof:circleimageview:2.1.0' }
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.github.mthli:Slice:v1.2'
// Conductor // Conductor
compile "com.bluelinelabs:conductor:2.1.4" implementation "com.bluelinelabs:conductor:2.1.4"
compile 'com.github.inorichi:conductor-support-preference:9e36460' implementation 'com.github.inorichi:conductor-support-preference:26.0.2'
// RxBindings // RxBindings
final rxbindings_version = '1.0.1' final rxbindings_version = '1.0.1'
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
// Tests // Tests
testCompile 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1' testImplementation 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.10.19' testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4' final robolectric_version = '3.1.4'
testCompile "org.robolectric:robolectric:$robolectric_version" testImplementation "org.robolectric:robolectric:$robolectric_version"
testCompile "org.robolectric:shadows-multidex:$robolectric_version" testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testCompile "org.robolectric:shadows-play-services:$robolectric_version" testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
final coroutines_version = '0.19.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
} }
buildscript { buildscript {
ext.kotlin_version = '1.1.3' ext.kotlin_version = '1.1.51'
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -230,49 +245,8 @@ repositories {
mavenCentral() mavenCentral()
} }
// Workaround to force a support lib version kotlin {
configurations.all { experimental {
resolutionStrategy.eachDependency { details -> coroutines 'enable'
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '25.4.0'
}
}
} }
} }
// add support for placeholders in resource files
//https://code.google.com/p/android/issues/detail?id=69224
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
def file = new File(basePath, fileName);
if (!file.exists()) {
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
return;
}
logger.debug("Replacing placeholders in " + file.toString())
logger.debug("Placeholders: " + placeholders.toString())
def content = file.getText('UTF-8')
placeholders.each { entry ->
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
}
file.write(content, 'UTF-8')
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processResources.doFirst {
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
}
}
}
}

View File

@ -6,6 +6,14 @@
-keep class com.hippo.image.** { *; } -keep class com.hippo.image.** { *; }
-keep interface com.hippo.image.** { *; } -keep interface com.hippo.image.** { *; }
-dontwarn nucleus.view.NucleusActionBarActivity
# Extensions may require methods unused in the core app
-keep class org.jsoup.** { *; }
-keep class kotlin.** { *; }
-keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; }
# OkHttp # OkHttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
@ -16,6 +24,7 @@
# Glide specific rules # # Glide specific rules #
# https://github.com/bumptech/glide # https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.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$** { -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES; **[] $VALUES;
public *; public *;

View File

@ -9,8 +9,7 @@
android:shortcutShortLabel="@string/label_library"> android:shortcutShortLabel="@string/label_library">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY" android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -21,8 +20,7 @@
android:shortcutShortLabel="@string/short_recent_updates"> android:shortcutShortLabel="@string/short_recent_updates">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -33,8 +31,7 @@
android:shortcutShortLabel="@string/label_recent_manga"> android:shortcutShortLabel="@string/label_recent_manga">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ" android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
@ -45,7 +42,6 @@
android:shortcutShortLabel="@string/label_catalogues"> android:shortcutShortLabel="@string/label_catalogues">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
android:targetPackage="${applicationId}" />
</shortcut> </shortcut>
</shortcuts> </shortcuts>

View File

@ -21,14 +21,14 @@
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTop">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/> <!--suppress AndroidDomInspection -->
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity> </activity>
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
@ -96,10 +96,6 @@
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false"/> android:exported="false"/>
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application> </application>
</manifest> </manifest>

View File

@ -7,6 +7,7 @@ import android.support.multidex.MultiDex
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.util.LocaleHelper import eu.kanade.tachiyomi.util.LocaleHelper
import org.acra.ACRA import org.acra.ACRA
@ -34,6 +35,7 @@ open class App : Application() {
setupAcra() setupAcra()
setupJobManager() setupJobManager()
setupNotificationChannels()
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
} }
@ -65,4 +67,8 @@ open class App : Application() {
} }
} }
protected open fun setupNotificationChannels() {
Notifications.createChannels(this)
}
} }

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi
object Constants {
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
const val NOTIFICATION_UPDATER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
}

View File

@ -45,6 +45,16 @@ object Migrations {
} }
} }
} }
if (oldVersion < 26) {
// Delete external chapter cache dir.
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
}
return true return true
} }
return false return false

View File

@ -84,9 +84,6 @@ class BackupCreateService : IntentService(NAME) {
// Create root object // Create root object
val root = JsonObject() val root = JsonObject()
// Create information object
val information = JsonObject()
// Create manga array // Create manga array
val mangaEntries = JsonArray() val mangaEntries = JsonArray()

View File

@ -29,7 +29,6 @@ class BackupCreatorJob : Job() {
if (interval > 0) { if (interval > 0) {
JobRequest.Builder(TAG) JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -182,29 +182,33 @@ class BackupRestoreService : Service() {
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> { private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) return Observable.just(Unit)
val json = JsonParser().parse(reader).asJsonObject .map {
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
// Get parser version // Get parser version
val version = json.get(VERSION)?.asInt ?: 1 val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager // Initialize manager
backupManager = BackupManager(this, version) backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0 restoreProgress = 0
errors.clear() errors.clear()
// Restore categories // Restore categories
json.get(CATEGORIES)?.let { json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray) backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
} }
return Observable.from(mangasJson) mangasJson
}
.flatMap { Observable.from(it) }
.concatMap { .concatMap {
val obj = it.asJsonObject val obj = it.asJsonObject
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
@ -317,8 +321,8 @@ class BackupRestoreService : Service() {
manga manga
} }
.filter { it.id != null } .filter { it.id != null }
.flatMap { manga -> .flatMap {
chapterFetchObservable(source, manga, chapters) chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters. // Convert to the manga that contains new chapters.
.map { manga } .map { manga }
} }

View File

@ -45,8 +45,7 @@ class ChapterCache(private val context: Context) {
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
/** Cache class used for cache management. */ /** Cache class used for cache management. */
private val diskCache = DiskLruCache.open( private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE) PARAMETER_CACHE_SIZE)
@ -82,10 +81,10 @@ class ChapterCache(private val context: Context) {
try { try {
// Remove the extension from the file to get the key of the cache // Remove the extension from the file to get the key of the cache
val key = file.substring(0, file.lastIndexOf(".")) val key = file.substringBeforeLast(".")
// Remove file from cache. // Remove file from cache.
return diskCache.remove(key) return diskCache.remove(key)
} catch (e: IOException) { } catch (e: Exception) {
return false return false
} }
} }

View File

@ -20,7 +20,8 @@ class CoverCache(private val context: Context) {
/** /**
* Cache directory used for cache management. * Cache directory used for cache management.
*/ */
private val cacheDir = context.getExternalFilesDir("covers") private val cacheDir = context.getExternalFilesDir("covers") ?:
File(context.filesDir, "covers").also { it.mkdirs() }
/** /**
* Returns the cover from cache. * Returns the cover from cache.

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 4 const val DATABASE_VERSION = 5
} }
override fun onCreate(db: SQLiteDatabase) = with(db) { override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
if (oldVersion < 4) { if (oldVersion < 4) {
db.execSQL(ChapterTable.bookmarkUpdateQuery) db.execSQL(ChapterTable.bookmarkUpdateQuery)
} }
if (oldVersion < 5) {
db.execSQL(ChapterTable.addScanlator)
}
} }
override fun onConfigure(db: SQLiteDatabase) { override fun onConfigure(db: SQLiteDatabase) {

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
put(COL_URL, obj.url) put(COL_URL, obj.url)
put(COL_NAME, obj.name) put(COL_NAME, obj.name)
put(COL_READ, obj.read) put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark) put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch) put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload) put(COL_DATE_UPLOAD, obj.date_upload)
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)) manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME)) name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1 read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1 bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH)) date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))

View File

@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
} }
} }
open class MangaGetResolver : DefaultGetResolver<Manga>() { interface BaseMangaGetResolver {
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID)) id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE)) source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL)) url = cursor.getString(cursor.getColumnIndex(COL_URL))
@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
} }
} }
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
override fun mapFromCursor(cursor: Cursor): Manga {
return mapBaseFromCursor(MangaImpl(), cursor)
}
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()

View File

@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
override lateinit var name: String override lateinit var name: String
override var scanlator: String? = null
override var read: Boolean = false override var read: Boolean = false
override var bookmark: Boolean = false override var bookmark: Boolean = false
@ -29,9 +31,7 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter val chapter = other as Chapter
return url == chapter.url return url == chapter.url
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var category: Int = 0
}

View File

@ -16,10 +16,6 @@ interface Manga : SManga {
var chapter_flags: Int var chapter_flags: Int
var unread: Int
var category: Int
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK) setFlags(order, SORT_MASK)
} }

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga { open class MangaImpl : Manga {
override var id: Long? = null override var id: Long? = null
@ -32,10 +32,6 @@ class MangaImpl : Manga {
override var chapter_flags: Int = 0 override var chapter_flags: Int = 0
@Transient override var unread: Int = 0
@Transient override var category: Int = 0
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false

View File

@ -4,6 +4,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
@ -23,7 +24,7 @@ interface MangaQueries : DbProvider {
.prepare() .prepare()
fun getLibraryMangas() = db.get() fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder() .withQuery(RawQuery.builder()
.query(libraryQuery) .query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)

View File

@ -1,24 +1,23 @@
package eu.kanade.tachiyomi.data.database.resolvers package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor import android.database.Cursor
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaGetResolver() { class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
companion object { companion object {
val INSTANCE = LibraryMangaGetResolver() val INSTANCE = LibraryMangaGetResolver()
} }
override fun mapFromCursor(cursor: Cursor): Manga { override fun mapFromCursor(cursor: Cursor): LibraryManga {
val manga = super.mapFromCursor(cursor) val manga = LibraryManga()
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD) mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(unreadColumn) manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga return manga
} }

View File

@ -14,6 +14,8 @@ object ChapterTable {
const val COL_READ = "read" const val COL_READ = "read"
const val COL_SCANLATOR = "scanlator"
const val COL_BOOKMARK = "bookmark" const val COL_BOOKMARK = "bookmark"
const val COL_DATE_FETCH = "date_fetch" const val COL_DATE_FETCH = "date_fetch"
@ -32,6 +34,7 @@ object ChapterTable {
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL, $COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL, $COL_NAME TEXT NOT NULL,
$COL_SCANLATOR TEXT,
$COL_READ BOOLEAN NOT NULL, $COL_READ BOOLEAN NOT NULL,
$COL_BOOKMARK BOOLEAN NOT NULL, $COL_BOOKMARK BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL, $COL_LAST_PAGE_READ INT NOT NULL,
@ -52,4 +55,7 @@ object ChapterTable {
val bookmarkUpdateQuery: String val bookmarkUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
} }

View File

@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import java.util.regex.Pattern import java.util.regex.Pattern
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
* Notification builder. * Notification builder.
*/ */
private val notification by lazy { private val notification by lazy {
NotificationCompat.Builder(context) NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
} }
@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) {
* *
* @param id the id of the notification. * @param id the id of the notification.
*/ */
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) { private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) {
* those can only be dismissed by the user. * those can only be dismissed by the user.
*/ */
fun dismiss() { fun dismiss() {
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
} }
/** /**
@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID) notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information // Reset download information
errorThrown = true errorThrown = true

View File

@ -122,7 +122,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state) .subscribe({ state -> onNetworkStateChanged(state)
}, { error -> }, { _ ->
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
}) })

View File

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.*
import kotlinx.coroutines.experimental.async
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -90,12 +91,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
@Volatile private var isRunning: Boolean = false @Volatile private var isRunning: Boolean = false
init { init {
Observable.fromCallable { store.restore() } launchNow {
.map { downloads -> downloads.filter { isDownloadAllowed(it) } } val chapters = async { store.restore() }
.subscribeOn(Schedulers.io()) queue.addAll(chapters.await())
.observeOn(AndroidSchedulers.mainThread()) }
.subscribe({ downloads -> queue.addAll(downloads)
}, { error -> Timber.e(error) })
} }
/** /**
@ -114,6 +113,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
val pending = queue.filter { it.status != Download.DOWNLOADED } val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
// Show download notification when simultaneous download > 1.
notifier.onProgressChange(queue)
downloadsRelay.call(pending) downloadsRelay.call(pending)
return !pending.isEmpty() return !pending.isEmpty()
} }
@ -210,61 +212,54 @@ class Downloader(private val context: Context, private val provider: DownloadPro
} }
/** /**
* Creates a download object for every chapter and adds them to the downloads queue. This method * Creates a download object for every chapter and adds them to the downloads queue.
* must be called in the main thread.
* *
* @param manga the manga of the chapters to download. * @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>) { fun queueChapters(manga: Manga, chapters: List<Chapter>) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
val chaptersToQueue = chapters // Called in background thread, the operation can be slow with SAF.
// Avoid downloading chapters with the same name. val chaptersWithoutDir = async {
.distinctBy { it.name } val mangaDir = provider.findMangaDir(source, manga)
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
// Create a downloader for each one.
.map { Download(source, manga, it) }
// Filter out those already queued or downloaded.
.filter { isDownloadAllowed(it) }
// Return if there's nothing to queue. chapters
if (chaptersToQueue.isEmpty()) // Avoid downloading chapters with the same name.
return .distinctBy { it.name }
// Filter out those already downloaded.
queue.addAll(chaptersToQueue) .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
// Add chapters to queue from the start.
// Initialize queue size. .sortedByDescending { it.source_order }
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
} }
}
/** // Runs in main thread (synchronization needed).
* Returns true if the given download can be queued and downloaded. val chaptersToQueue = chaptersWithoutDir.await()
* // Filter out those already enqueued.
* @param download the download to be checked. .filter { chapter -> queue.none { it.chapter.id == chapter.id } }
*/ // Create a download for each one.
private fun isDownloadAllowed(download: Download): Boolean { .map { Download(source, manga, it) }
// If the chapter is already queued, don't add it again
if (queue.any { it.chapter.id == download.chapter.id })
return false
val dir = provider.findChapterDir(download.source, download.manga, download.chapter) if (chaptersToQueue.isNotEmpty()) {
if (dir != null && dir.exists()) queue.addAll(chaptersToQueue)
return false
return true // Initialize queue size.
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
}
// Start downloader if needed
DownloadService.start(this@Downloader.context)
}
} }
/** /**
@ -292,7 +287,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
} }
return pageListObservable return pageListObservable
.doOnNext { pages -> .doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles() tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") } ?.filter { it.name!!.endsWith(".tmp") }
@ -308,7 +303,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download, queue) } .doOnNext { notifier.onProgressChange(download, queue) }
.toList() .toList()
.map { pages -> download } .map { _ -> download }
// Do after download completes // Do after download completes
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here

View File

@ -66,6 +66,7 @@ class DownloadQueue(
val pageStatusSubject = PublishSubject.create<Int>() val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject) setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject return@flatMap pageStatusSubject
.onBackpressureBuffer()
.filter { it == Page.READY } .filter { it == Page.READY }
.map { download } .map { download }

View File

@ -1,35 +1,51 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority import android.content.ContentValues.TAG
import com.bumptech.glide.load.data.DataFetcher import android.util.Log
import java.io.File import com.bumptech.glide.Priority
import java.io.IOException import com.bumptech.glide.load.DataSource
import java.io.InputStream import com.bumptech.glide.load.data.DataFetcher
import java.io.*
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null
private var data: InputStream? = null
override fun loadData(priority: Priority): InputStream {
data = file.inputStream() override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
return data!! loadFromFile(callback)
} }
override fun cleanup() { protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
data?.let { data -> try {
try { data = FileInputStream(file)
data.close() } catch (e: FileNotFoundException) {
} catch (e: IOException) { if (Log.isLoggable(TAG, Log.DEBUG)) {
// Ignore Log.d(TAG, "Failed to open file", e)
} }
} callback.onLoadFailed(e)
} return
}
override fun cancel() {
// Do nothing. callback.onDataReady(data)
} }
override fun getId(): String { override fun cleanup() {
return file.toString() try {
} data?.close()
} catch (e: IOException) {
// Ignored.
}
}
override fun cancel() {
// Do nothing.
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
} }

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a library manga.
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
* and copies the result to the cache.
*
* @param networkFetcher the network fetcher for this cover.
* @param manga the manga of the cover to load.
* @param file the file where this cover should be. It may exists or not.
*/
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga,
private val file: File)
: FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file)
loadFromFile(callback)
} catch (e: Exception) {
tmpFile.delete()
callback.onLoadFailed(e)
}
} else {
callback.onLoadFailed(Exception("Null data"))
}
}
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
} else {
loadFromFile(callback)
}
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
override fun cancel() {
super.cancel()
networkFetcher.cancel()
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
/**
* Returns the id for this manga's cover.
*
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
* the file has changed. If the file doesn't exist it will append a 0.
*/
override fun getId(): String {
return manga.thumbnail_url + file.lastModified()
}
}

View File

@ -1,23 +1,24 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.util.LruCache import android.util.LruCache
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.* import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
/** /**
* A class for loading a cover associated with a [Manga] that can be present in our own cache. * A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow: * Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
* *
* - Check in RAM LRU. * - Check in RAM LRU.
* - Check in disk LRU. * - Check in disk LRU.
@ -26,7 +27,7 @@ import java.io.InputStream
* *
* @param context the application context. * @param context the application context.
*/ */
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> { class MangaModelLoader : ModelLoader<Manga, InputStream> {
/** /**
* Cover cache where persistent covers are stored. * Cover cache where persistent covers are stored.
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
/** /**
* Base network loader. * Default network client.
*/ */
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java, private val defaultClient = Injekt.get<NetworkHelper>().client
InputStream::class.java, context)
/** /**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url * LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite. * and the file where it should be stored in case the manga is a favorite.
*/ */
private val lruCache = LruCache<String, Pair<GlideUrl, File>>(100) private val lruCache = LruCache<GlideUrl, File>(100)
/** /**
* Map where request headers are stored for a source. * Map where request headers are stored for a source.
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*/ */
class Factory : ModelLoaderFactory<Manga, InputStream> { class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory) override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
= MangaModelLoader(context) return MangaModelLoader()
}
override fun teardown() {} override fun teardown() {}
} }
override fun handles(model: Manga): Boolean {
return true
}
/** /**
* Returns a fetcher for the given manga or null if the url is empty. * Returns a fetcher for the given manga or null if the url is empty.
* *
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
* @param width the width of the view where the resource will be loaded. * @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded. * @param height the height of the view where the resource will be loaded.
*/ */
override fun getResourceFetcher(manga: Manga, override fun buildLoadData(manga: Manga, width: Int, height: Int,
width: Int, options: Options?): ModelLoader.LoadData<InputStream>? {
height: Int): DataFetcher<InputStream>? {
// Check thumbnail is not null or empty // Check thumbnail is not null or empty
val url = manga.thumbnail_url val url = manga.thumbnail_url
if (url == null || url.isEmpty()) { if (url == null || url.isEmpty()) {
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
if (url.startsWith("http")) { if (url.startsWith("http")) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache.
val (glideUrl, file) = lruCache.get(url) ?:
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
lruCache.put(url, this)
}
// Get the resource fetcher for this request url. // Get the resource fetcher for this request url.
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) } val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
if (!manga.favorite) {
return ModelLoader.LoadData(glideUrl, networkFetcher)
}
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return MangaUrlFetcher(networkFetcher, file, manga) return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} else { } else {
// Get the file from the url, removing the scheme if present. // Get the file from the url, removing the scheme if present.
val file = File(url.substringAfter("file://")) val file = File(url.substringAfter("file://"))
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return MangaFileFetcher(file, manga) return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
} }
} }
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
} }
} }
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}
} }

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.security.MessageDigest
class MangaSignature(manga: Manga, file: File) : Key {
private val key = manga.thumbnail_url + file.lastModified()
override fun equals(other: Any?): Boolean {
return if (other is MangaSignature) {
key == other.key
} else {
false
}
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET))
}
}

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
* fallbacks to network and copies it to the cache.
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
* to network for fetching.
*
* @param networkFetcher the network fetcher for this cover.
* @param file the file where this cover should be. It may exists or not.
* @param manga the manga of the cover to load.
*/
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: MangaFileFetcher(file, manga) {
override fun loadData(priority: Priority): InputStream {
if (manga.favorite) {
synchronized(file) {
if (!file.exists()) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve source stream.
val input = networkFetcher.loadData(priority)
?: throw Exception("Couldn't open source stream")
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
input.use { output.use { input.copyTo(output) } }
tmpFile.renameTo(file)
} catch (e: Exception) {
tmpFile.delete()
throw e
}
}
}
return super.loadData(priority)
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
}

View File

@ -1,12 +1,18 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -16,17 +22,20 @@ import java.io.InputStream
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
*/ */
class AppGlideModule : GlideModule { @GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024)) builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java,
DrawableTransitionOptions.withCrossFade())
} }
override fun registerComponents(context: Context, glide: Glide) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory) registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
} }
} }

View File

@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
.setRequiredNetworkType(wifiRestriction) .setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction) .setRequiresCharging(acRestriction)
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -10,16 +10,17 @@ import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@ -80,10 +81,12 @@ class LibraryUpdateService(
/** /**
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
private val progressNotification by lazy { NotificationCompat.Builder(this) private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
.setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img) .setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap) .setLargeIcon(notificationBitmap)
.setOngoing(true) .setOngoing(true)
.setOnlyAlertOnce(true)
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
} }
@ -132,7 +135,11 @@ class LibraryUpdateService(
putExtra(KEY_TARGET, target) putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) } category?.let { putExtra(KEY_CATEGORY, it.id) }
} }
context.startService(intent) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
} }
} }
@ -153,6 +160,7 @@ class LibraryUpdateService(
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire() wakeLock.acquire()
@ -224,7 +232,7 @@ class LibraryUpdateService(
* @param target the target to update. * @param target the target to update.
* @return a list of manga to update * @return a list of manga to update
*/ */
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1) var listToUpdate = if (categoryId != -1)
@ -255,7 +263,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> { fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
// List containing new updates // List containing new updates
@ -279,7 +287,7 @@ class LibraryUpdateService(
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(manga)
Pair(emptyList<Chapter>(), emptyList<Chapter>()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
@ -347,7 +355,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> { fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
@ -358,7 +366,7 @@ class LibraryUpdateService(
// Update the details of the manga. // Update the details of the manga.
.concatMap { manga -> .concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<Manga>() ?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
@ -377,7 +385,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a * Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here. * background thread, so it's safe to do heavy operations or network calls here.
*/ */
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> { private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
var count = 0 var count = 0
@ -417,7 +425,7 @@ class LibraryUpdateService(
* @param total the total progress. * @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title) .setContentTitle(manga.title)
.setProgress(total, current, false) .setProgress(total, current, false)
.build()) .build())
@ -434,7 +442,7 @@ class LibraryUpdateService(
// Append new chapters from a previous, existing notification // Append new chapters from a previous, existing notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val previousNotification = notificationManager.activeNotifications val previousNotification = notificationManager.activeNotifications
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID } .find { it.id == Notifications.ID_LIBRARY_RESULT }
if (previousNotification != null) { if (previousNotification != null) {
val oldUpdates = previousNotification.notification.extras val oldUpdates = previousNotification.notification.extras
@ -446,7 +454,7 @@ class LibraryUpdateService(
} }
} }
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification { notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
setSmallIcon(R.drawable.ic_book_white_24dp) setSmallIcon(R.drawable.ic_book_white_24dp)
setLargeIcon(notificationBitmap) setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters)) setContentTitle(getString(R.string.notification_new_chapters))
@ -466,7 +474,7 @@ class LibraryUpdateService(
* Cancels the progress notification. * Cancels the progress notification.
*/ */
private fun cancelProgressNotification() { private fun cancelProgressNotification() {
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
} }
/** /**

View File

@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context) ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
// Clear the download queue // Clear the download queue
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true) ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
// Show message notification created
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
// Launch share activity and dismiss notification // Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION), ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Cancel library update and dismiss notification // Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID) ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
// Open reader activity // Open reader activity
ACTION_OPEN_CHAPTER -> { ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1), openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to clear downloads. // Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS" private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to notify user shortcut is created.
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
// Called to dismiss notification. // Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION" private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHORTCUT_CREATED
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/** /**
* Returns [PendingIntent] that starts a service which dismissed the notification * Returns [PendingIntent] that starts a service which dismissed the notification
* *

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.data.notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
/**
* Class to manage the basic information of all the notifications used in the app.
*/
object Notifications {
/**
* Common notification channel and ids used anywhere.
*/
const val CHANNEL_COMMON = "common_channel"
const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2
/**
* Notification channel and ids used by the library updater.
*/
const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = 101
const val ID_LIBRARY_RESULT = 102
/**
* Notification channel and ids used by the downloader.
*/
const val CHANNEL_DOWNLOADER = "downloader_channel"
const val ID_DOWNLOAD_CHAPTER = 201
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
/**
* Creates the notification channels introduced in Android Oreo.
*
* @param context The application context.
*/
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channels = listOf(
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW)
)
context.notificationManager.createNotificationChannels(channels)
}
}

View File

@ -87,6 +87,8 @@ object PreferenceKeys {
const val filterUnread = "pref_filter_unread_key" const val filterUnread = "pref_filter_unread_key"
const val filterCompleted = "pref_filter_completed_key"
const val librarySortingMode = "library_sorting_mode" const val librarySortingMode = "library_sorting_mode"
const val automaticUpdates = "automatic_updates" const val automaticUpdates = "automatic_updates"
@ -103,6 +105,8 @@ object PreferenceKeys {
const val defaultCategory = "default_category" const val defaultCategory = "default_category"
const val downloadBadge = "display_download_badge"
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId" fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId" fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"

View File

@ -141,10 +141,14 @@ class PreferencesHelper(val context: Context) {
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false) fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false) fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false) fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0) fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true) fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)

View File

@ -6,8 +6,8 @@ import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job import com.evernote.android.job.Job
import com.evernote.android.job.JobManager import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
class UpdateCheckerJob : Job() { class UpdateCheckerJob : Job() {
@ -23,7 +23,7 @@ class UpdateCheckerJob : Job() {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url) putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
} }
NotificationCompat.Builder(context).update { NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name)) setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available)) setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
@ -43,7 +43,7 @@ class UpdateCheckerJob : Job() {
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) { fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block() block()
context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build()) context.notificationManager.notify(Notifications.ID_UPDATER, build())
} }
companion object { companion object {
@ -54,7 +54,6 @@ class UpdateCheckerJob : Job() {
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000) .setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true) .setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true) .setUpdateCurrent(true)
.build() .build()
.schedule() .schedule()

View File

@ -4,10 +4,10 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.notificationManager
import java.io.File import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
@ -49,7 +49,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
/** /**
* Notification shown to user * Notification shown to user
*/ */
private val notification = NotificationCompat.Builder(context) private val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) { when (intent.getStringExtra(EXTRA_ACTION)) {
@ -82,6 +82,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
private fun updateProgress(progress: Int) { private fun updateProgress(progress: Int) {
with(notification) { with(notification) {
setProgress(100, progress, false) setProgress(100, progress, false)
setOnlyAlertOnce(true)
} }
notification.show() notification.show()
} }
@ -96,6 +97,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
with(notification) { with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete)) setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done) setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Install action // Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path))) setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
@ -105,7 +107,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
// Cancel action // Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img, addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
} }
notification.show() notification.show()
} }
@ -120,6 +122,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
with(notification) { with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error)) setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
// Retry action // Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img, addAction(R.drawable.ic_refresh_grey_24dp_img,
@ -128,7 +131,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
// Cancel action // Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img, addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel), context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID)) NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
} }
notification.show() notification.show()
} }
@ -138,7 +141,7 @@ internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceive
* *
* @param id the id of the notification. * @param id the id of the notification.
*/ */
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) { private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
} }

View File

@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) {
val cookies = value as? Set<String> val cookies = value as? Set<String>
if (cookies != null) { if (cookies != null) {
try { try {
val url = HttpUrl.parse("http://$key") val url = HttpUrl.parse("http://$key") ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) } val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() } .filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies) cookieMap.put(key, nonExpiredCookies)

View File

@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
override val id = ID override val id = ID
override val name = "LocalSource" override val name = context.getString(R.string.local_source)
override val lang = "" override val lang = ""
override val supportsLatest = true override val supportsLatest = true
@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) { when (state?.index) {
0 -> { 0 -> {
if (state!!.ascending) if (state.ascending)
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
else else
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) } mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
} }
1 -> { 1 -> {
if (state!!.ascending) if (state.ascending)
mangaDirs = mangaDirs.sortedBy(File::lastModified) mangaDirs = mangaDirs.sortedBy(File::lastModified)
else else
mangaDirs = mangaDirs.sortedByDescending(File::lastModified) mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} else { } else {
chapterFile.nameWithoutExtension chapterFile.nameWithoutExtension
} }
val chapNameCut = chapName.replace(manga.title, "", true).trim() val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_')
name = if (chapNameCut.isEmpty()) chapName else chapNameCut name = if (chapNameCut.isEmpty()) chapName else chapNameCut
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
ChapterRecognition.parseChapterNumber(this, manga) ChapterRecognition.parseChapterNumber(this, manga)

View File

@ -28,7 +28,11 @@ class Page(
@Transient private var statusSubject: Subject<Int, Int>? = null @Transient private var statusSubject: Subject<Int, Int>? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = (100 * bytesRead / contentLength).toInt() progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
} }
fun setStatusSubject(subject: Subject<Int, Int>?) { fun setStatusSubject(subject: Subject<Int, Int>?) {

View File

@ -12,11 +12,14 @@ interface SChapter : Serializable {
var chapter_number: Float var chapter_number: Float
var scanlator: String?
fun copyFrom(other: SChapter) { fun copyFrom(other: SChapter) {
name = other.name name = other.name
url = other.url url = other.url
date_upload = other.date_upload date_upload = other.date_upload
chapter_number = other.chapter_number chapter_number = other.chapter_number
scanlator = other.scanlator
} }
companion object { companion object {

View File

@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
override var chapter_number: Float = -1f override var chapter_number: Float = -1f
override var scanlator: String? = null
} }

View File

@ -13,6 +13,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource {
override val id by lazy { override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId" val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
} }
/** /**
@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to * Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method. * override this method. If a manga is licensed an empty chapter list observable is returned
* *
* @param manga the manga to look for chapters. * @param manga the manga to look for chapters.
*/ */
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga)) if (manga.status != SManga.LICENSED) {
.asObservableSuccess() return client.newCall(chapterListRequest(manga))
.map { response -> .asObservableSuccess()
chapterListParse(response) .map { response ->
} chapterListParse(response)
}
} else {
return Observable.error(Exception("Licensed - No chapters to show"))
}
} }
/** /**

View File

@ -69,7 +69,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
element.select("a[href^=$baseUrl]").first().let { element.select("a[href*=bato.to]").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().trim() manga.title = it.text().trim()
} }
@ -189,7 +189,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun chapterListSelector() = "tr.row.lang_English.chapter_row" override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a[href^=$baseUrl/reader").first() val urlElement = element.select("a[href*=bato.to/reader").first()
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
@ -197,6 +197,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
chapter.date_upload = element.select("td").getOrNull(4)?.let { chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it) parseDateFromElement(it)
} ?: 0 } ?: 0
chapter.scanlator = element.select("td").getOrNull(2)?.text()
return chapter return chapter
} }

View File

@ -11,6 +11,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.regex.Pattern import java.util.regex.Pattern
@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() {
val manga = SManga.create() val manga = SManga.create()
element.select("td a:eq(0)").first().let { element.select("td a:eq(0)").first().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text() val title = it.text()
//check if cloudfire email obfuscation is affecting title name
if (title.contains("[email protected]", true)) {
try {
var str: String = it.html()
//get the number
str = str.substringAfter("data-cfemail=\"")
str = str.substringBefore("\">[email")
val sb = StringBuilder()
//convert number to char
val r = Integer.valueOf(str.substring(0, 2), 16)!!
var i = 2
while (i < str.length) {
val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar()
sb.append(c)
i += 2
}
//replace the new word into the title
manga.title = title.replace("[email protected]", sb.toString(), true)
} catch (e: Exception) {
//on error just default to obfuscated title
Timber.e("error parsing [email protected]", e)
manga.title = title
}
} else {
manga.title = title
}
} }
return manga return manga
} }
@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() {
Genre("Mystery"), Genre("Mystery"),
Genre("One shot"), Genre("One shot"),
Genre("Psychological"), Genre("Psychological"),
Genre("Reincarnation"),
Genre("Romance"), Genre("Romance"),
Genre("School Life"), Genre("School Life"),
Genre("Sci-fi"), Genre("Sci-fi"),
@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() {
Genre("Smut"), Genre("Smut"),
Genre("Sports"), Genre("Sports"),
Genre("Supernatural"), Genre("Supernatural"),
Genre("Time Travel"),
Genre("Tragedy"), Genre("Tragedy"),
Genre("Transported"),
Genre("Webtoon"), Genre("Webtoon"),
Genre("Yaoi"), Genre("Yaoi"),
Genre("Yuri") Genre("Yuri")

View File

@ -61,7 +61,7 @@ class Mangafox : ParsedHttpSource() {
is Status -> url.addQueryParameter(filter.id, filter.state.toString()) is Status -> url.addQueryParameter(filter.id, filter.state.toString())
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) } is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
is TextField -> url.addQueryParameter(filter.key, filter.state) is TextField -> url.addQueryParameter(filter.key, filter.state)
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString()) is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
is OrderBy -> { is OrderBy -> {
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index]) url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za") url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() {
val infoElement = document.select("div#title").first() val infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first() val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first() val sideInfoElement = document.select("#series_info").first()
val licensedElement = document.select("div.warning").first()
val manga = SManga.create() val manga = SManga.create()
manga.author = rowElement.select("td:eq(1)").first()?.text() manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text() manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text() manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text() manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) } val isLicensed = licensedElement?.text()?.contains("licensed")
if (isLicensed == true) {
manga.status = SManga.LICENSED
} else {
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
}
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src") manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
return manga return manga
} }
@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() {
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0 chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter return chapter
} }
@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by", private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false)) Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -7,9 +7,13 @@ import okhttp3.HttpUrl
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
class Mangahere : ParsedHttpSource() { class Mangahere : ParsedHttpSource() {
@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
private val trustManager = object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray()
}
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
}
private val sslContext = SSLContext.getInstance("SSL").apply {
init(null, arrayOf(trustManager), SecureRandom())
}
override val client = super.client.newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
override fun popularMangaSelector() = "div.directory_list > ul > li" override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun latestUpdatesSelector() = "div.directory_list > ul > li" override fun latestUpdatesSelector() = "div.directory_list > ul > li"
@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() {
val infoElement = detailElement.select(".detail_topText").first() val infoElement = detailElement.select(".detail_topText").first()
val manga = SManga.create() val manga = SManga.create()
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text() manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text() manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):") manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less") manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) } manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach { document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value"))) if (!it.attr("value").contains("featured.html")) {
pages.add(Page(pages.size, "http:" + it.attr("value")))
}
} }
pages.getOrNull(0)?.imageUrl = imageUrlParse(document) pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages return pages
@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by", private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"), arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false)) Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(

View File

@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
override val name = "ReadMangaToday" override val name = "ReadMangaToday"
override val baseUrl = "http://www.readmanga.today" override val baseUrl = "http://www.readmng.com/"
override val lang = "en" override val lang = "en"
@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() {
return pages return pages
} }
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src") override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src")
private class Status : Filter.TriState("Completed") private class Status : Filter.TriState("Completed")
private class Genre(name: String, val id: Int) : Filter.TriState(name) private class Genre(name: String, val id: Int) : Filter.TriState(name)

View File

@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers) GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var pageNum = 1 var pageNum = 1
@ -48,9 +47,7 @@ class Mangachan : ParsedHttpSource() {
return GET(url, headers) return GET(url, headers)
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
return GET("$baseUrl/newestch?page=$page")
}
override fun popularMangaSelector() = "div.content_row" override fun popularMangaSelector() = "div.content_row"
@ -76,9 +73,7 @@ class Mangachan : ParsedHttpSource() {
return manga return manga
} }
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a:contains(Вперед)" override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@ -125,16 +120,14 @@ class Mangachan : ParsedHttpSource() {
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text() manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text()) manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text() manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src") manga.thumbnail_url = imgElement.attr("src")
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("перевод завершен") -> SManga.COMPLETED
element.contains("перевод завершен") -> return SManga.COMPLETED element.contains("перевод продолжается") -> SManga.ONGOING
element.contains("перевод продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "table.table_cha tr:gt(1)" override fun chapterListSelector() = "table.table_cha tr:gt(1)"

View File

@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaSelector() = "div.desc" override fun popularMangaSelector() = "div.desc"
@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() {
return manga return manga
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga =
return popularMangaFromElement(element) popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search?q=$query&$genres", headers) return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() {
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://mintmanga.com/search * on http://mintmanga.com/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Genre("арт", "el_2220"), Genre("арт", "el_2220"),
@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() {
Genre("меха", "el_1318"), Genre("меха", "el_1318"),
Genre("мистика", "el_1324"), Genre("мистика", "el_1324"),
Genre("научная фантастика", "el_1325"), Genre("научная фантастика", "el_1325"),
Genre("омегаверс", "el_5676"),
Genre("повседневность", "el_1327"), Genre("повседневность", "el_1327"),
Genre("постапокалиптика", "el_1342"), Genre("постапокалиптика", "el_1342"),
Genre("приключения", "el_1322"), Genre("приключения", "el_1322"),

View File

@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() {
override fun latestUpdatesSelector() = "div.desc" override fun latestUpdatesSelector() = "div.desc"
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request =
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers) GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() {
return manga return manga
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga =
return popularMangaFromElement(element) popularMangaFromElement(element)
}
override fun popularMangaNextPageSelector() = "a.nextLink" override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink" override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&") val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search?q=$query&$genres", headers) return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
} }
override fun searchMangaSelector() = popularMangaSelector() override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
return popularMangaFromElement(element)
}
// max 200 results // max 200 results
override fun searchMangaNextPageSelector() = null override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
return manga return manga
} }
private fun parseStatus(element: String): Int { private fun parseStatus(element: String): Int = when {
when { element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING else -> SManga.UNKNOWN
else -> return SManga.UNKNOWN
}
} }
override fun chapterListSelector() = "div.chapters-link tbody tr" override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => { /* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33); * const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n') * return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://readmanga.me/search * on http://readmanga.me/search/advanced
*/ */
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
Genre("арт", "el_5685"), Genre("арт", "el_5685"),

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@ -32,7 +33,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null return null
} }
private fun setTitle() { fun setTitle() {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) { if (parentController is BaseController && parentController.getTitle() != null) {
@ -44,4 +45,22 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() (activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
} }
/**
* Workaround for disappearing menu items when collapsing an expandable item like a SearchView.
* This method should be removed when fixed upstream.
* Issue link: https://issuetracker.google.com/issues/37657375
*/
fun MenuItem.fixExpand() {
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
activity?.invalidateOptionsMenu()
return true
}
})
}
} }

View File

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.ui.base.controller package eu.kanade.tachiyomi.ui.base.controller
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
fun Router.popControllerWithTag(tag: String): Boolean { fun Router.popControllerWithTag(tag: String): Boolean {
@ -9,4 +13,15 @@ fun Router.popControllerWithTag(tag: String): Boolean {
return true return true
} }
return false return false
}
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
val activity = activity ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissions.forEach { permission ->
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
requestPermissions(arrayOf(permission), requestCode)
}
}
}
} }

View File

@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter import nucleus.presenter.Presenter
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(), abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory<P> { PresenterFactory<P> {
private val delegate = NucleusConductorDelegate(this) private val delegate = NucleusConductorDelegate(this)

View File

@ -10,7 +10,6 @@ public class NucleusConductorDelegate<P extends Presenter> {
@Nullable private P presenter; @Nullable private P presenter;
@Nullable private Bundle bundle; @Nullable private Bundle bundle;
private boolean presenterHasView = false;
private PresenterFactory<P> factory; private PresenterFactory<P> factory;
@ -22,8 +21,8 @@ public class NucleusConductorDelegate<P extends Presenter> {
if (presenter == null) { if (presenter == null) {
presenter = factory.createPresenter(); presenter = factory.createPresenter();
presenter.create(bundle); presenter.create(bundle);
bundle = null;
} }
bundle = null;
return presenter; return presenter;
} }
@ -37,31 +36,26 @@ public class NucleusConductorDelegate<P extends Presenter> {
} }
void onRestoreInstanceState(Bundle presenterState) { void onRestoreInstanceState(Bundle presenterState) {
if (presenter != null)
throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
bundle = presenterState; bundle = presenterState;
} }
void onTakeView(Object view) { void onTakeView(Object view) {
getPresenter(); getPresenter();
if (presenter != null && !presenterHasView) { if (presenter != null) {
//noinspection unchecked //noinspection unchecked
presenter.takeView(view); presenter.takeView(view);
presenterHasView = true;
} }
} }
void onDropView() { void onDropView() {
if (presenter != null && presenterHasView) { if (presenter != null) {
presenter.dropView(); presenter.dropView();
presenterHasView = false;
} }
} }
void onDestroy() { void onDestroy() {
if (presenter != null) { if (presenter != null) {
presenter.destroy(); presenter.destroy();
presenter = null;
} }
} }
} }

View File

@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.Snackbar import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.* import android.support.v7.widget.*
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@ -43,14 +39,18 @@ import java.util.concurrent.TimeUnit
/** /**
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class CatalogueController(bundle: Bundle? = null) : open class CatalogueController(bundle: Bundle) :
NucleusController<CataloguePresenter>(bundle), NucleusController<CataloguePresenter>(bundle),
SecondaryDrawerController, SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener<ProgressItem>, FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
})
/** /**
* Preferences helper. * Preferences helper.
*/ */
@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var adapter: FlexibleAdapter<IFlexible<*>>? = null private var adapter: FlexibleAdapter<IFlexible<*>>? = null
/**
* Spinner shown in the toolbar to change the selected source.
*/
private var spinner: Spinner? = null
/** /**
* Snackbar containing an error message when a request fails. * Snackbar containing an error message when a request fails.
*/ */
@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private var recycler: RecyclerView? = null private var recycler: RecyclerView? = null
/**
* Drawer listener to allow swipe only for closing the drawer.
*/
private var drawerListener: DrawerLayout.DrawerListener? = null private var drawerListener: DrawerLayout.DrawerListener? = null
/**
* Query of the search box.
*/
private val query: String
get() = presenter.query
/**
* Selected index of the spinner (selected source).
*/
private var selectedIndex: Int = 0
/** /**
* Subscription for the search view. * Subscription for the search view.
*/ */
private var searchViewSubscription: Subscription? = null private var searchViewSubscription: Subscription? = null
/**
* Subscription for the number of manga per row.
*/
private var numColumnsSubscription: Subscription? = null private var numColumnsSubscription: Subscription? = null
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null private var progressItem: ProgressItem? = null
init { init {
@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
} }
override fun getTitle(): String? { override fun getTitle(): String? {
return "" return presenter.source.name
} }
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): CataloguePresenter {
return CataloguePresenter() return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this) adapter = FlexibleAdapter(null, this)
setupRecycler(view) setupRecycler(view)
// Create toolbar spinner navView?.setFilters(presenter.filterItems)
val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
?: activity
val spinnerAdapter = ArrayAdapter(themedContext,
android.R.layout.simple_spinner_item, presenter.sources)
spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
val onItemSelected: (Int) -> Unit = { position ->
val source = spinnerAdapter.getItem(position)
if (!presenter.isValidSource(source)) {
spinner?.setSelection(selectedIndex)
activity?.toast(R.string.source_requires_login)
} else if (source != presenter.source) {
selectedIndex = position
showProgressBar()
adapter?.clear()
presenter.setActiveSource(source)
navView?.setFilters(presenter.filterItems)
activity?.invalidateOptionsMenu()
}
}
selectedIndex = presenter.sources.indexOf(presenter.source)
spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter
setSelection(selectedIndex)
itemSelections()
.skip(1)
.filter { it != AdapterView.INVALID_POSITION }
.subscribeUntilDestroy { onItemSelected(it) }
}
activity?.toolbar?.addView(spinner)
view.progress?.visible() view.progress?.visible()
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
super.onDestroyView(view) super.onDestroyView(view)
activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe() numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null numColumnsSubscription = null
searchViewSubscription?.unsubscribe() searchViewSubscription?.unsubscribe()
searchViewSubscription = null searchViewSubscription = null
adapter = null adapter = null
spinner = null
snack = null snack = null
recycler = null recycler = null
} }
@ -187,10 +144,7 @@ open class CatalogueController(bundle: Bundle? = null) :
} }
navView.setFilters(presenter.filterItems) navView.setFilters(presenter.filterItems)
navView.post { drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
if (isAttached && !drawer.isDrawerOpen(navView))
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
}
navView.onSearchClicked = { navView.onSearchClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
@ -228,6 +182,7 @@ open class CatalogueController(bundle: Bundle? = null) :
val recycler = if (presenter.isListMode) { val recycler = if (presenter.isListMode) {
RecyclerView(view.context).apply { RecyclerView(view.context).apply {
id = R.id.recycler
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
} }
@ -267,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply { menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView val searchView = actionView as SearchView
val query = presenter.query
if (!query.isBlank()) { if (!query.isBlank()) {
expandActionView() expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
@ -330,9 +286,14 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
private fun searchWithQuery(newQuery: String) { private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query == newQuery) if (presenter.query == newQuery)
return return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
if (newQuery == "") {
activity?.invalidateOptionsMenu()
}
showProgressBar() showProgressBar()
adapter?.clear() adapter?.clear()
@ -444,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/ */
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> { fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
presenter.prefs.portraitColumns() preferences.portraitColumns()
else else
presenter.prefs.landscapeColumns() preferences.landscapeColumns()
} }
/** /**
@ -555,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
presenter.updateMangaCategories(manga, categories) presenter.updateMangaCategories(manga, categories)
} }
protected companion object {
const val SOURCE_ID_KEY = "sourceId"
}
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
@ -36,16 +36,15 @@ class CatalogueGridHolder(private val view: View, private val adapter: FlexibleA
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)
.into(StateImageViewTarget(view.thumbnail, view.progress)) .into(StateImageViewTarget(view.thumbnail, view.progress))
} }
} }
} }

View File

@ -1,39 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import com.f2prateek.rx.preferences.Preference
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_grid_item.view.* import kotlinx.android.synthetic.main.catalogue_grid_item.view.*
class CatalogueItem(val manga: Manga) : AbstractFlexibleItem<CatalogueHolder>() { class CatalogueItem(val manga: Manga, private val catalogueAsList: Preference<Boolean>) :
AbstractFlexibleItem<CatalogueHolder>() {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.catalogue_grid_item return if (catalogueAsList.getOrDefault())
R.layout.catalogue_list_item
else
R.layout.catalogue_grid_item
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueHolder {
inflater: LayoutInflater, val parent = adapter.recyclerView
parent: ViewGroup): CatalogueHolder { return if (parent is AutofitRecyclerView) {
view.apply {
if (parent is AutofitRecyclerView) {
val view = parent.inflate(R.layout.catalogue_grid_item).apply {
card.layoutParams = FrameLayout.LayoutParams( card.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4) MATCH_PARENT, parent.itemWidth / 3 * 4)
gradient.layoutParams = FrameLayout.LayoutParams( gradient.layoutParams = FrameLayout.LayoutParams(
MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM) MATCH_PARENT, parent.itemWidth / 3 * 4 / 2, Gravity.BOTTOM)
} }
return CatalogueGridHolder(view, adapter) CatalogueGridHolder(view, adapter)
} else { } else {
val view = parent.inflate(R.layout.catalogue_list_item) CatalogueListHolder(view, adapter)
return CatalogueListHolder(view, adapter)
} }
} }

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import kotlinx.android.synthetic.main.catalogue_list_item.view.* import kotlinx.android.synthetic.main.catalogue_list_item.view.*
@ -36,12 +36,13 @@ class CatalogueListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
} }
override fun setImage(manga: Manga) { override fun setImage(manga: Manga) {
Glide.clear(view.thumbnail) GlideApp.with(view.context).clear(view.thumbnail)
if (!manga.thumbnail_url.isNullOrEmpty()) { if (!manga.thumbnail_url.isNullOrEmpty()) {
Glide.with(view.context) GlideApp.with(view.context)
.load(manga) .load(manga)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop() .centerCrop()
.circleCrop()
.dontAnimate() .dontAnimate()
.skipMemoryCache(true) .skipMemoryCache(true)
.placeholder(android.R.color.transparent) .placeholder(android.R.color.transparent)

View File

@ -34,7 +34,7 @@ class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs:
} }
fun setFilters(items: List<IFlexible<*>>) { fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items.toMutableList()) adapter.updateDataSet(items)
} }
} }

View File

@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.* import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController]. * Presenter of [CatalogueController].
*/ */
open class CataloguePresenter( open class CataloguePresenter(
val sourceManager: SourceManager = Injekt.get(), sourceId: Long,
val db: DatabaseHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get(),
val prefs: PreferencesHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
val coverCache: CoverCache = Injekt.get() private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : BasePresenter<CatalogueController>() {
/** /**
* Enabled sources. * Selected source.
*/ */
val sources by lazy { getEnabledSources() } val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Active source.
*/
lateinit var source: CatalogueSource
private set
/** /**
* Query from the view. * Query from the view.
@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
source = getLastUsedSource()
sourceFilters = source.getFilterList() sourceFilters = source.getFilterList()
if (savedState != null) { if (savedState != null) {
@ -141,17 +131,19 @@ open class CataloguePresenter(
val sourceId = source.id val sourceId = source.id
val catalogueAsList = prefs.catalogueAsList()
// Prepare the pager. // Prepare the pager.
pagerSubscription?.let { remove(it) } pagerSubscription?.let { remove(it) }
pagerSubscription = pager.results() pagerSubscription = pager.results()
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } } .map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
.doOnNext { initializeMangas(it.second) } .doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) } .map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeReplay({ view, pair -> .subscribeReplay({ view, (page, mangas) ->
view.onAddPage(pair.first, pair.second) view.onAddPage(page, mangas)
}, { view, error -> }, { _, error ->
Timber.e(error) Timber.e(error)
}) })
@ -167,7 +159,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) } pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() } pageSubscription = Observable.defer { pager.requestNext() }
.subscribeFirst({ view, page -> .subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted. // Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError) }, CatalogueController::onAddPageError)
} }
@ -179,19 +171,6 @@ open class CataloguePresenter(
return pager.hasNextPage return pager.hasNextPage
} }
/**
* Sets the active source and restarts the pager.
*
* @param source the new active source.
*/
fun setActiveSource(source: CatalogueSource) {
prefs.lastUsedCatalogueSource().set(source.id)
this.source = source
sourceFilters = source.getFilterList()
restartPager(query = "", filters = FilterList())
}
/** /**
* Sets the display mode. * Sets the display mode.
* *
@ -267,50 +246,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) } .onErrorResumeNext { Observable.just(manga) }
} }
/**
* Returns the last used source from preferences or the first valid source.
*
* @return a source.
*/
fun getLastUsedSource(): CatalogueSource {
val id = prefs.lastUsedCatalogueSource().get() ?: -1
val source = sourceManager.get(id)
if (!isValidSource(source) || source !in sources) {
return sources.first { isValidSource(it) }
}
return source as CatalogueSource
}
/**
* Checks if the given source is valid.
*
* @param source the source to check.
* @return true if the source is valid, false otherwise.
*/
open fun isValidSource(source: Source?): Boolean {
if (source == null) return false
if (source is LoginSource) {
return source.isLogged() ||
(prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
}
return true
}
/**
* Returns a list of enabled sources ordered by language and name.
*/
open protected fun getEnabledSources(): List<CatalogueSource> {
val languages = prefs.enabledLanguages().getOrDefault()
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
/** /**
* Adds or removes a manga from the library. * Adds or removes a manga from the library.
* *
@ -370,13 +305,12 @@ open class CataloguePresenter(
} }
is Filter.Sort -> { is Filter.Sort -> {
val group = SortGroup(it) val group = SortGroup(it)
val subItems = it.values.mapNotNull { val subItems = it.values.map {
SortItem(it, group) SortItem(it, group)
} }
group.subItems = subItems group.subItems = subItems
group group
} }
else -> null
} }
} }
} }
@ -407,7 +341,7 @@ open class CataloguePresenter(
* @param categories the selected categories. * @param categories the selected categories.
* @param manga the manga to move. * @param manga the manga to move.
*/ */
fun moveMangaToCategories(manga: Manga, categories: List<Category>) { private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga)) db.setMangaCategories(mc, listOf(manga))
} }

View File

@ -1,30 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() { class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
var loadMore = true private var loadMore = true
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.catalogue_progress_item return R.layout.catalogue_progress_item
} }
override fun createViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: Holder, position: Int, payloads: List<Any?>) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>) {
holder.progressBar.visibility = View.GONE holder.progressBar.visibility = View.GONE
holder.progressMessage.visibility = View.GONE holder.progressMessage.visibility = View.GONE
@ -45,8 +42,8 @@ class ProgressItem : AbstractFlexibleItem<ProgressItem.Holder>() {
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val progressBar = view.findViewById(R.id.progress_bar) as ProgressBar val progressBar: ProgressBar = view.findViewById(R.id.progress_bar)
val progressMessage = view.findViewById(R.id.progress_message) as TextView val progressMessage: TextView = view.findViewById(R.id.progress_message)
} }
} }

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -16,8 +14,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
return R.layout.navigation_view_checkbox return R.layout.navigation_view_checkbox
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,10 +30,8 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is CheckboxItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as CheckboxItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -44,6 +40,6 @@ open class CheckboxItem(val filter: Filter.CheckBox) : AbstractFlexibleItem<Chec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val check = itemView.findViewById(R.id.nav_view_item) as CheckBox val check: CheckBox = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -19,8 +17,12 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
return R.layout.navigation_view_group return R.layout.navigation_view_group
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return 101
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -34,10 +36,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is GroupItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as GroupItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -46,8 +46,8 @@ class GroupItem(val filter: Filter.Group<*>) : AbstractExpandableHeaderItem<Grou
open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) { open class Holder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter, true) {
val title = itemView.findViewById(R.id.title) as TextView val title: TextView = itemView.findViewById(R.id.title)
val icon = itemView.findViewById(R.id.expand_icon) as ImageView val icon: ImageView = itemView.findViewById(R.id.expand_icon)
override fun shouldNotifyParentOnClick(): Boolean { override fun shouldNotifyParentOnClick(): Boolean {
return true return true

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.design.R import android.support.design.R
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractHeaderItem
@ -18,8 +16,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
return R.layout.design_navigation_item_subheader return R.layout.design_navigation_item_subheader
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -29,10 +27,8 @@ class HeaderItem(val filter: Filter.Header) : AbstractHeaderItem<HeaderItem.Hold
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is HeaderItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as HeaderItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -15,10 +15,8 @@ class TriStateSectionItem(filter: Filter.TriState) : TriStateItem(filter), ISect
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TriStateSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TriStateSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -38,10 +36,8 @@ class TextSectionItem(filter: Filter.Text) : TextItem(filter), ISectionable<Text
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TextSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TextSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -61,10 +57,8 @@ class CheckboxSectionItem(filter: Filter.CheckBox) : CheckboxItem(filter), ISect
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is CheckboxSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as CheckboxSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -84,10 +78,8 @@ class SelectSectionItem(filter: Filter.Select<*>) : SelectItem(filter), ISection
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SelectSectionItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SelectSectionItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
@ -19,8 +17,8 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
return R.layout.navigation_view_spinner return R.layout.navigation_view_spinner
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,18 +30,16 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
android.R.layout.simple_spinner_item, filter.values).apply { android.R.layout.simple_spinner_item, filter.values).apply {
setDropDownViewResource(R.layout.common_spinner_item) setDropDownViewResource(R.layout.common_spinner_item)
} }
spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { position -> spinner.onItemSelectedListener = IgnoreFirstSpinnerListener { pos ->
filter.state = position filter.state = pos
} }
spinner.setSelection(filter.state) spinner.setSelection(filter.state)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SelectItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SelectItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -52,7 +48,7 @@ open class SelectItem(val filter: Filter.Select<*>) : AbstractFlexibleItem<Selec
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(R.id.nav_view_item_text) as TextView val text: TextView = itemView.findViewById(R.id.nav_view_item_text)
val spinner = itemView.findViewById(R.id.nav_view_item) as Spinner val spinner: Spinner = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.support.design.R import android.support.design.R
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
@ -17,8 +15,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
return R.layout.design_navigation_item_separator return R.layout.design_navigation_item_separator
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -27,10 +25,8 @@ class SeparatorItem(val filter: Filter.Separator) : AbstractHeaderItem<Separator
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SeparatorItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SeparatorItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.ISectionable import eu.davidea.flexibleadapter.items.ISectionable
@ -12,13 +10,16 @@ import eu.kanade.tachiyomi.util.setVectorCompat
class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() { class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGroup.Holder, ISectionable<*, *>>() {
// Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.id.catalogue_filter_sort_group return R.layout.navigation_view_group
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(R.layout.navigation_view_group, parent, false), adapter) return 100
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -32,10 +33,8 @@ class SortGroup(val filter: Filter.Sort) : AbstractExpandableHeaderItem<SortGrou
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SortGroup) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as SortGroup).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView import android.widget.CheckedTextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem
@ -15,13 +13,16 @@ import eu.kanade.tachiyomi.util.getResourceColor
class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) { class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem<SortItem.Holder, SortGroup>(group) {
// Use an id instead of the layout res to allow to reuse the layout.
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.id.catalogue_filter_sort_item return R.layout.navigation_view_checkedtext
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(R.layout.navigation_view_checkedtext, parent, false), adapter) return 102
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -54,10 +55,9 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is SortItem) { if (javaClass != other?.javaClass) return false
return name == other.name && group == other.group other as SortItem
} return name == other.name && group == other.group
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -68,7 +68,7 @@ class SortItem(val name: String, val group: SortGroup) : AbstractSectionableItem
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(R.id.nav_view_item) as CheckedTextView val text: CheckedTextView = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.ui.catalogue.filter package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.widget.TextInputLayout import android.support.design.widget.TextInputLayout
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.EditText import android.widget.EditText
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -18,8 +16,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
return R.layout.navigation_view_text return R.layout.navigation_view_text
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -34,10 +32,8 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TextItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TextItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -46,7 +42,7 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val wrapper = itemView.findViewById(R.id.nav_view_item_wrapper) as TextInputLayout val wrapper: TextInputLayout = itemView.findViewById(R.id.nav_view_item_wrapper)
val edit = itemView.findViewById(R.id.nav_view_item) as EditText val edit: EditText = itemView.findViewById(R.id.nav_view_item)
} }
} }

View File

@ -2,9 +2,7 @@ package eu.kanade.tachiyomi.ui.catalogue.filter
import android.support.design.R import android.support.design.R
import android.support.graphics.drawable.VectorDrawableCompat import android.support.graphics.drawable.VectorDrawableCompat
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.CheckedTextView import android.widget.CheckedTextView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -20,8 +18,12 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
return TR.layout.navigation_view_checkedtext return TR.layout.navigation_view_checkedtext
} }
override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, parent: ViewGroup?): Holder { override fun getItemViewType(): Int {
return Holder(inflater.inflate(layoutRes, parent, false), adapter) return 103
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder {
return Holder(view, adapter)
} }
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, position: Int, payloads: List<Any?>?) {
@ -51,10 +53,8 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other is TriStateItem) { if (javaClass != other?.javaClass) return false
return filter == other.filter return filter == (other as TriStateItem).filter
}
return false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -63,7 +63,7 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) { class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView val text: CheckedTextView = itemView.findViewById(TR.id.nav_view_item)
init { init {
// Align with native checkbox // Align with native checkbox

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.os.Parcelable
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import eu.davidea.flexibleadapter.FlexibleAdapter
/**
* Adapter that holds the search cards.
*
* @param controller instance of [CatalogueSearchController].
*/
class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
FlexibleAdapter<CatalogueSearchItem>(null, controller, true) {
/**
* Bundle where the view state of the holders is saved.
*/
private var bundle = Bundle()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List<Any?>?) {
super.onBindViewHolder(holder, position, payloads)
restoreHolderState(holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
saveHolderState(holder, bundle)
}
override fun onSaveInstanceState(outState: Bundle) {
val holdersBundle = Bundle()
allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)
}
/**
* Saves the view state of the given holder.
*
* @param holder The holder to save.
* @param outState The bundle where the state is saved.
*/
private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
val key = "holder_${holder.adapterPosition}"
val holderState = SparseArray<Parcelable>()
holder.itemView.saveHierarchyState(holderState)
outState.putSparseParcelableArray(key, holderState)
}
/**
* Restores the view state of the given holder.
*
* @param holder The holder to restore.
*/
private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
val key = "holder_${holder.adapterPosition}"
val holderState = bundle.getSparseParcelableArray<Parcelable>(key)
if (holderState != null) {
holder.itemView.restoreHierarchyState(holderState)
bundle.remove(key)
}
}
private companion object {
const val HOLDER_BUNDLE_KEY = "holder_bundle"
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
/**
* Adapter that holds the manga items from search results.
*
* @param controller instance of [CatalogueSearchController].
*/
class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
FlexibleAdapter<CatalogueSearchCardItem>(null, controller, true) {
/**
* Listen for browse item clicks.
*/
val mangaClickListener: OnMangaClickListener = controller
/**
* Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueSearchController]
*/
interface OnMangaClickListener {
fun onMangaClick(manga: Manga)
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
: FlexibleViewHolder(view, adapter) {
init {
// Call onMangaClickListener when item is pressed.
itemView.setOnClickListener {
val item = adapter.getItem(adapterPosition)
if (item != null) {
adapter.mangaClickListener.onMangaClick(item.manga)
}
}
}
fun bind(manga: Manga) {
itemView.tvTitle.text = manga.title
setImage(manga)
}
fun setImage(manga: Manga) {
GlideApp.with(itemView.context).clear(itemView.itemImage)
if (!manga.thumbnail_url.isNullOrEmpty()) {
GlideApp.with(itemView.context)
.load(manga)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.centerCrop()
.skipMemoryCache(true)
.placeholder(android.R.color.transparent)
.into(StateImageViewTarget(itemView.itemImage, itemView.progress))
}
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem<CatalogueSearchCardHolder>() {
override fun getLayoutRes(): Int {
return R.layout.catalogue_global_search_controller_card_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchCardHolder {
return CatalogueSearchCardHolder(view, adapter as CatalogueSearchCardAdapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(manga)
}
override fun equals(other: Any?): Boolean {
if (other is CatalogueSearchCardItem) {
return manga.id == other.manga.id
}
return false
}
override fun hashCode(): Int {
return manga.id?.toInt() ?: 0
}
}

View File

@ -0,0 +1,184 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.*
/**
* This controller shows and manages the different search result in global search.
* This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
* [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/
class CatalogueSearchController(private val initialQuery: String? = null) :
NucleusController<CatalogueSearchPresenter>(),
CatalogueSearchCardAdapter.OnMangaClickListener {
/**
* Adapter containing search results grouped by lang.
*/
private var adapter: CatalogueSearchAdapter? = null
/**
* Called when controller is initialized.
*/
init {
setHasOptionsMenu(true)
}
/**
* Initiate the view with [R.layout.catalogue_global_search_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View {
return inflater.inflate(R.layout.catalogue_global_search_controller, container, false)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return presenter.query
}
/**
* Create the [CatalogueSearchPresenter] used in controller.
*
* @return instance of [CatalogueSearchPresenter]
*/
override fun createPresenter(): CatalogueSearchPresenter {
return CatalogueSearchPresenter(initialQuery)
}
/**
* Called when manga in global search is clicked, opens manga.
*
* @param manga clicked item containing manga information.
*/
override fun onMangaClick(manga: Manga) {
// Open MangaController.
router.pushController(RouterTransaction.with(MangaController(manga, true))
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.catalogue_new_list, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
presenter.search(it.queryText().toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
}
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueSearchAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onSaveViewState(view: View, outState: Bundle) {
super.onSaveViewState(view, outState)
adapter?.onSaveInstanceState(outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
adapter?.onRestoreInstanceState(savedViewState)
}
/**
* Returns the view holder for the given manga.
*
* @param source used to find holder containing source
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? {
val adapter = adapter ?: return null
adapter.allBoundViewHolders.forEach { holder ->
val item = adapter.getItem(holder.adapterPosition)
if (item != null && source.id == item.source.id) {
return holder as CatalogueSearchHolder
}
}
return null
}
/**
* Add search result to adapter.
*
* @param searchResult result of search.
*/
fun setItems(searchResult: List<CatalogueSearchItem>) {
adapter?.updateDataSet(searchResult)
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the initialized manga.
*/
fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
getHolder(source)?.setImage(manga)
}
}

View File

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.support.v7.widget.LinearLayoutManager
import android.view.View
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.setVectorCompat
import eu.kanade.tachiyomi.util.visible
import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
/**
* Holder that binds the [CatalogueSearchItem] containing catalogue cards.
*
* @param view view of [CatalogueSearchItem]
* @param adapter instance of [CatalogueSearchAdapter]
*/
class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) {
/**
* Adapter containing manga from search results.
*/
private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller)
private var lastBoundResults: List<CatalogueSearchCardItem>? = null
init {
with(itemView) {
// Set layout horizontal.
recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = mangaAdapter
nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
context.getResourceColor(android.R.attr.textColorHint))
}
}
/**
* Show the loading of source search result.
*
* @param item item of card.
*/
fun bind(item: CatalogueSearchItem) {
val source = item.source
val results = item.results
with(itemView) {
// Set Title witch country code if available.
title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
when {
results == null -> {
progress.visible()
nothing_found.gone()
}
results.isEmpty() -> {
progress.gone()
nothing_found.visible()
}
else -> {
progress.gone()
nothing_found.gone()
}
}
if (results !== lastBoundResults) {
mangaAdapter.updateDataSet(results)
lastBoundResults = results
}
}
}
/**
* Called from the presenter when a manga is initialized.
*
* @param manga the initialized manga.
*/
fun setImage(manga: Manga) {
getHolder(manga)?.setImage(manga)
}
/**
* Returns the view holder for the given manga.
*
* @param manga the manga to find.
* @return the holder of the manga or null if it's not bound.
*/
private fun getHolder(manga: Manga): CatalogueSearchCardHolder? {
mangaAdapter.allBoundViewHolders.forEach { holder ->
val item = mangaAdapter.getItem(holder.adapterPosition)
if (item != null && item.manga.id!! == manga.id!!) {
return holder as CatalogueSearchCardHolder
}
}
return null
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
/**
* Item that contains search result information.
*
* @param source contains information about search result.
*/
class CatalogueSearchItem(val source: CatalogueSource, val results: List<CatalogueSearchCardItem>?)
: AbstractFlexibleItem<CatalogueSearchHolder>() {
/**
* Set view.
*
* @return id of view
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_global_search_controller_card
}
/**
* Create view holder (see [CatalogueSearchAdapter].
*
* @return holder of view.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): CatalogueSearchHolder {
return CatalogueSearchHolder(view, adapter as CatalogueSearchAdapter)
}
/**
* Bind item to view.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
/**
* Used to check if two items are equal.
*
* @return items are equal?
*/
override fun equals(other: Any?): Boolean {
if (other is CatalogueSearchItem) {
return source.id == other.source.id
}
return false
}
/**
* Return hash code of item.
*
* @return hashcode
*/
override fun hashCode(): Int {
return source.id.toInt()
}
}

View File

@ -0,0 +1,215 @@
package eu.kanade.tachiyomi.ui.catalogue.global_search
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Presenter of [CatalogueSearchController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param db manages the database calls.
* @param preferencesHelper manages the preference calls.
*/
class CatalogueSearchPresenter(
val initialQuery: String? = "",
val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(),
val preferencesHelper: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueSearchController>() {
/**
* Enabled sources.
*/
val sources by lazy { getEnabledSources() }
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/
private var fetchSourcesSubscription: Subscription? = null
/**
* Subject which fetches image of given manga.
*/
private val fetchImageSubject = PublishSubject.create<Pair<List<Manga>, Source>>()
/**
* Subscription for fetching images of manga.
*/
private var fetchImageSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Perform a search with previous or initial state
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
}
override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe()
super.onDestroy()
}
override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query)
super.onSave(state)
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferencesHelper.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it is LoginSource && !it.isLogged() }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" }
}
/**
* Initiates a search for mnaga per catalogue.
*
* @param query query on which to search.
*/
fun search(query: String) {
// Return if there's nothing to do
if (this.query == query) return
// Update query
this.query = query
// Create image fetch subscription
initializeFetchImageSubscription()
// Create items with the initial state
val initialItems = sources.map { CatalogueSearchItem(it, null) }
var items = initialItems
fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(sources)
.flatMap({ source ->
source.fetchSearchManga(1, query, FilterList())
.subscribeOn(Schedulers.io())
.onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
.map { it.mangas.take(10) } // Get at most 10 manga from search result.
.map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
.doOnNext { fetchImage(it, source) } // Load manga covers.
.map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) }
}, 5)
.observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results
.map { result ->
items.map { item -> if (item.source == result.source) result else item }
}
// Update current state
.doOnNext { items = it }
// Deliver initial state
.startWith(initialItems)
.subscribeLatestCache({ view, manga ->
view.setItems(manga)
}, { _, error ->
Timber.e(error)
})
}
/**
* Initialize a list of manga.
*
* @param manga the list of manga to initialize.
*/
private fun fetchImage(manga: List<Manga>, source: Source) {
fetchImageSubject.onNext(Pair(manga, source))
}
/**
* Subscribes to the initializer of manga details and updates the view if needed.
*/
private fun initializeFetchImageSubscription() {
fetchImageSubscription?.unsubscribe()
fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
.flatMap {
val source = it.second
Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized }
.map { Pair(it, source) }
.concatMap { getMangaDetailsObservable(it.first, it.second) }
.map { Pair(source as CatalogueSource, it) }
}
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ (source, manga) ->
@Suppress("DEPRECATION")
view?.onMangaInitialized(source, manga)
}, { error ->
Timber.e(error)
})
}
/**
* Returns an observable of manga that initializes the given manga.
*
* @param manga the manga to initialize.
* @return an observable of the manga to initialize
*/
private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable<Manga> {
return source.fetchMangaDetails(manga)
.flatMap { networkManga ->
manga.copyFrom(networkManga)
manga.initialized = true
db.insertManga(manga).executeAsBlocking()
Observable.just(manga)
}
.onErrorResumeNext { Observable.just(manga) }
}
/**
* Returns a manga from the database for the given manga from network. It creates a new entry
* if the manga is not yet in the database.
*
* @param sManga the manga from the source.
* @return a manga from the database.
*/
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
newManga.copyFrom(sManga)
val result = db.insertManga(newManga).executeAsBlocking()
newManga.id = result.insertedId()
localManga = newManga
}
return localManga
}
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.getResourceColor
/**
* Adapter that holds the catalogue cards.
*
* @param controller instance of [CatalogueMainController].
*/
class CatalogueMainAdapter(val controller: CatalogueMainController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
init {
setDisplayHeadersAtStartUp(true)
}
/**
* Listener for browse item clicks.
*/
val browseClickListener: OnBrowseClickListener = controller
/**
* Listener for latest item clicks.
*/
val latestClickListener: OnLatestClickListener = controller
/**
* Listener which should be called when user clicks browse.
* Note: Should only be handled by [CatalogueMainController]
*/
interface OnBrowseClickListener {
fun onBrowseClick(position: Int)
}
/**
* Listener which should be called when user clicks latest.
* Note: Should only be handled by [CatalogueMainController]
*/
interface OnLatestClickListener {
fun onLatestClick(position: Int)
}
}

View File

@ -0,0 +1,238 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
* [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
* [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
*/
class CatalogueMainController : NucleusController<CatalogueMainPresenter>(),
SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
CatalogueMainAdapter.OnBrowseClickListener,
CatalogueMainAdapter.OnLatestClickListener {
/**
* Application preferences.
*/
private val preferences: PreferencesHelper = Injekt.get()
/**
* Adapter containing sources.
*/
private var adapter : CatalogueMainAdapter? = null
/**
* Called when controller is initialized.
*/
init {
// Enable the option menu
setHasOptionsMenu(true)
}
/**
* Set the title of controller.
*
* @return title.
*/
override fun getTitle(): String? {
return applicationContext?.getString(R.string.label_catalogues)
}
/**
* Create the [CatalogueMainPresenter] used in controller.
*
* @return instance of [CatalogueMainPresenter]
*/
override fun createPresenter(): CatalogueMainPresenter {
return CatalogueMainPresenter()
}
/**
* Initiate the view with [R.layout.catalogue_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
}
/**
* Called when the view is created
*
* @param view view of controller
* @param savedViewState information from previous state.
*/
override fun onViewCreated(view: View, savedViewState: Bundle?) {
super.onViewCreated(view, savedViewState)
adapter = CatalogueMainAdapter(this)
with(view) {
// Create recycler and set adapter.
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(context))
}
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
presenter.updateSources()
}
}
/**
* Called when login dialog is closed, refreshes the adapter.
*
* @param source clicked item containing source information.
*/
override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
adapter?.clear()
presenter.loadSources()
}
}
/**
* Called when item is clicked
*/
override fun onItemClick(position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val source = item.source
if (source is LoginSource && !source.isLogged()) {
val dialog = SourceLoginDialog(source)
dialog.targetController = this
dialog.showDialog(router)
} else {
// Open the catalogue view.
openCatalogue(source, CatalogueController(source))
}
return false
}
/**
* Called when browse is clicked in [CatalogueMainAdapter]
*/
override fun onBrowseClick(position: Int) {
onItemClick(position)
}
/**
* Called when latest is clicked in [CatalogueMainAdapter]
*/
override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source))
}
/**
* Opens a catalogue with the given controller.
*/
private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
preferences.lastUsedCatalogueSource().set(source.id)
router.pushController(RouterTransaction.with(controller)
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.catalogue_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextChangeEvents()
.filter { it.isSubmitted }
.subscribeUntilDestroy {
val query = it.queryText().toString()
router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
}
/**
* Called when an option menu item has been selected by the user.
*
* @param item The selected item.
* @return True if this event has been consumed, false if it has not.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
// Initialize option to open catalogue settings.
R.id.action_settings -> {
router.pushController((RouterTransaction.with(SettingsSourcesController()))
.popChangeHandler(SettingsSourcesFadeChangeHandler())
.pushChangeHandler(FadeChangeHandler()))
}
else -> return super.onOptionsItemSelected(item)
}
return true
}
/**
* Called to update adapter containing sources.
*/
fun setSources(sources: List<IFlexible<*>>) {
adapter?.updateDataSet(sources)
}
/**
* Called to set the last used catalogue at the top of the view.
*/
fun setLastUsedSource(item: SourceItem?) {
adapter?.removeAllScrollableHeaders()
if (item != null) {
adapter?.addScrollableHeader(item)
}
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
}

View File

@ -0,0 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Bundle
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Presenter of [CatalogueMainController]
* Function calls should be done from here. UI calls should be done from the controller.
*
* @param sourceManager manages the different sources.
* @param preferences application preferences.
*/
class CatalogueMainPresenter(
val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) : BasePresenter<CatalogueMainController>() {
/**
* Enabled sources.
*/
var sources = getEnabledSources()
/**
* Subscription for retrieving enabled sources.
*/
private var sourceSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Load enabled and last used sources
loadSources()
loadLastUsedSource()
}
/**
* Unsubscribe and create a new subscription to fetch enabled sources.
*/
fun loadSources() {
sourceSubscription?.unsubscribe()
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
// Catalogues without a lang defined will be placed at the end
when {
d1 == "" && d2 != "" -> 1
d2 == "" && d1 != "" -> -1
else -> d1.compareTo(d2)
}
}
val byLang = sources.groupByTo(map, { it.lang })
val sourceItems = byLang.flatMap {
val langItem = LangItem(it.key)
it.value.map { source -> SourceItem(source, langItem) }
}
sourceSubscription = Observable.just(sourceItems)
.subscribeLatestCache(CatalogueMainController::setSources)
}
private fun loadLastUsedSource() {
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
// Emit the first item immediately but delay subsequent emissions by 500ms.
Observable.merge(
sharedObs.take(1),
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
.distinctUntilChanged()
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
.subscribeLatestCache(CatalogueMainController::setLastUsedSource)
}
fun updateSources() {
sources = getEnabledSources()
loadSources()
}
/**
* Returns a list of enabled sources ordered by language and name.
*
* @return list containing enabled sources.
*/
private fun getEnabledSources(): List<CatalogueSource> {
val languages = preferences.enabledLanguages().getOrDefault()
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
return sourceManager.getCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in hiddenCatalogues }
.sortedBy { "(${it.lang}) ${it.name}" } +
sourceManager.get(LocalSource.ID) as LocalSource
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) {
itemView.title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source)
else -> {
val locale = Locale(item.code)
locale.getDisplayName(locale).capitalize()
}
}
}
}

View File

@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.kanade.tachiyomi.R
/**
* Item that contains the language header.
*
* @param code The lang code.
*/
data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): LangHolder {
return LangHolder(view, adapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView
import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(ATTRS)
divider = a.getDrawable(0)
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider)
}
}

View File

@ -0,0 +1,107 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.os.Build
import android.view.View
import android.view.ViewGroup
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.util.dpToPx
import eu.kanade.tachiyomi.util.getRound
import eu.kanade.tachiyomi.util.gone
import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
private val slice = Slice(itemView.card).apply {
setColor(adapter.cardBackground)
}
init {
itemView.source_browse.setOnClickListener {
adapter.browseClickListener.onBrowseClick(adapterPosition)
}
itemView.source_latest.setOnClickListener {
adapter.latestClickListener.onLatestClick(adapterPosition)
}
}
fun bind(item: SourceItem) {
val source = item.source
with(itemView) {
setCardEdges(item)
// Set source name
title.text = source.name
// Set circle letter image.
post {
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
}
// If source is login, show only login option
if (source is LoginSource && !source.isLogged()) {
source_browse.setText(R.string.login)
source_latest.gone()
} else {
source_browse.setText(R.string.browse)
source_latest.visible()
}
}
}
private fun setCardEdges(item: SourceItem) {
// Position of this item in its header. Defaults to 0 when header is null.
var position = 0
// Number of items in the header of this item. Defaults to 1 when header is null.
var count = 1
if (item.header != null) {
val sectionItems = mAdapter.getSectionItems(item.header)
position = sectionItems.indexOf(item)
count = sectionItems.size
}
when {
// Only one item in the card
count == 1 -> applySlice(2f, false, false, true, true)
// First item of the card
position == 0 -> applySlice(2f, false, true, true, false)
// Last item of the card
position == count - 1 -> applySlice(2f, true, false, false, true)
// Middle item
else -> applySlice(0f, false, false, false, false)
}
}
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
topShadow: Boolean, bottomShadow: Boolean) {
slice.setRadius(radius)
slice.showLeftTopRect(topRect)
slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
val v = itemView.card
if (v.layoutParams is ViewGroup.MarginLayoutParams) {
val p = v.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)
}
}
companion object {
val margin = 8.dpToPx
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.ui.catalogue.main
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
/**
* Item that contains source information.
*
* @param source Instance of [CatalogueSource] containing source information.
* @param header The header for this item.
*/
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
AbstractSectionableItem<SourceHolder, LangItem>(header) {
/**
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.catalogue_main_controller_card_item
}
/**
* Creates a new view holder for this item.
*/
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
return SourceHolder(view, adapter as CatalogueMainAdapter)
}
/**
* Binds this item to the given view holder.
*/
override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
position: Int, payloads: List<Any?>?) {
holder.bind(this)
}
}

View File

@ -20,14 +20,14 @@ class CategoryAdapter(controller: CategoryController) :
*/ */
override fun clearSelection() { override fun clearSelection() {
super.clearSelection() super.clearSelection()
(0 until itemCount).forEach { getItem(it).isSelected = false } (0 until itemCount).forEach { getItem(it)?.isSelected = false }
} }
/** /**
* Clears the active selections from the model. * Clears the active selections from the model.
*/ */
fun clearModelSelection() { fun clearModelSelection() {
selectedPositions.forEach { getItem(it).isSelected = false } selectedPositions.forEach { getItem(it)?.isSelected = false }
} }
/** /**
@ -37,7 +37,7 @@ class CategoryAdapter(controller: CategoryController) :
*/ */
override fun toggleSelection(position: Int) { override fun toggleSelection(position: Int) {
super.toggleSelection(position) super.toggleSelection(position)
getItem(position).isSelected = isSelected(position) getItem(position)?.isSelected = isSelected(position)
} }
interface OnItemReleaseListener { interface OnItemReleaseListener {

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