Compare commits

...

90 Commits

Author SHA1 Message Date
Jobobby04 7557369d1f Release 1.2.0 2020-08-12 15:06:34 -04:00
Jobobby04 3e1804da8a Lazily instantiate some variables in EHSourceHelpers 2020-08-12 14:20:32 -04:00
Jobobby04 2ab0927313 Add PervEden to clean titles 2020-08-12 14:04:00 -04:00
Jobobby04 98b6a221fd NHentai/Hitomi say "All" when toString is called 2020-08-12 13:45:25 -04:00
Jobobby04 4733a62980 Wrap hitomi upload date in a Try/Catch to solve errors 2020-08-12 13:41:03 -04:00
Jobobby04 a5276fdadc When refreshing in groups update globally(tmep fix, hopefully grouping will update the group later) 2020-08-12 01:19:52 -04:00
Jobobby04 906ac9e00c Fix bugs with migrating manga with the action mode 2020-08-12 01:18:45 -04:00
arkon 8ae3b8e313 Update issue templates
(cherry picked from commit aa607e0ecb)
2020-08-12 00:34:18 -04:00
arkon ce36e6b242 Split out NSFW source setting to separate section
Temporarily hidden until feature is ready for stable release.

(cherry picked from commit 65b32ddeb2)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
2020-08-12 00:34:10 -04:00
arkon cfd3d59516 Fix Chinese plural string
(cherry picked from commit 5e9bdc2690)
2020-08-12 00:33:33 -04:00
Jozef Hollý ae915d7823 Translated using Weblate (Swedish) (#3580)
Currently translated at 99.8% (573 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Finnish)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Swedish)

Currently translated at 99.6% (572 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Greek)

Currently translated at 99.4% (571 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Russian)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Spanish)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Dutch)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Yakut)

Currently translated at 74.8% (428 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Filipino)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Finnish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Russian)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Spanish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (Turkish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (German)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Indonesian)

Currently translated at 99.6% (568 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Filipino)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.1% (565 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/

Translated using Weblate (Finnish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Greek)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Turkish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Russian)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Dutch)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Malay)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Spanish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 99.4% (567 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.8% (364 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.8% (364 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.5% (362 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.5% (362 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.3% (361 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.3% (361 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 62.4% (356 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 62.4% (356 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.5% (351 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.0% (342 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.9% (336 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.9% (336 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Malay)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Russian)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Spanish)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Yakut)

Currently translated at 49.6% (281 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Portuguese)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/

Translated using Weblate (Yakut)

Currently translated at 32.6% (185 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 10.4% (59 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Persian)

Currently translated at 96.6% (547 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/

Added translation using Weblate (Yakut)

Translated using Weblate (Catalan)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/

Translated using Weblate (Russian)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Spanish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Greek)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Dutch)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Czech)

Currently translated at 64.1% (363 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Italian)

Currently translated at 98.7% (559 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Sardinian)

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/

Translated using Weblate (Filipino)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Turkish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/

Translated using Weblate (Finnish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.4% (512 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (German)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Sardinian)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Filipino)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.3% (505 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.6% (563 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Portuguese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/

Translated using Weblate (Catalan)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/

Translated using Weblate (Greek)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Filipino)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Dutch)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Finnish)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Turkish)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Japanese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/

Translated using Weblate (Japanese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/

Translated using Weblate (German)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Malay)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
(cherry picked from commit 9ce994168a)
2020-08-12 00:33:25 -04:00
arkon b4b4497e7e Lift toolbar on scroll in extension details and manga controllers
(cherry picked from commit 748a720199)
2020-08-12 00:33:17 -04:00
arkon 19055e1699 Allow annotating SourceFactory with @Nsfw to block all sources within it
(cherry picked from commit 8db34eb3dd)
2020-08-12 00:33:07 -04:00
arkon f006467138 Add 18+ warnings in extensions list
(cherry picked from commit b657bba96e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionHolder.kt
2020-08-12 00:32:57 -04:00
Jobobby04 a740f79b56 Update readme 2020-08-12 00:28:37 -04:00
Jobobby04 6c388ae906 Cleanup images, replace some icons 2020-08-12 00:23:18 -04:00
Jobobby04 3fa5322133 Delegate NHentai, to continue using NHentai download the extension, SY requires NHentai version 1.2.28 2020-08-12 00:23:18 -04:00
Jobobby04 5a1bc6e25b Delegate Perv Eden, to continue using it download the extensions(there is a English extension and a Italian extension) 2020-08-12 00:21:29 -04:00
Jobobby04 51d0a67908 Account for custom delegated image requests, fixes Hitomi pages 2020-08-11 15:20:11 -04:00
Jobobby04 69f4d1fd46 Likely fix Hitomi extension 2020-08-11 01:35:36 -04:00
Jobobby04 8b53568fc8 Cleanup some hitomi leftovers 2020-08-10 23:47:26 -04:00
Jobobby04 d978b6d088 Update readme 2020-08-10 23:31:32 -04:00
Jobobby04 9a3fdc23e6 Delegate hitomi, it is now the first fully delegated factory source. To continue using hitomi please download the extension. This comes with a lot of fixes for future delegated factory sources 2020-08-10 23:29:10 -04:00
Jobobby04 f8efe5d189 Make a new debug function to help delegation 2020-08-10 21:15:48 -04:00
Jobobby04 aae23f5ef3 Delegate 8Muses, please manually migrate over your comics to the extension, as the old version of the 8Muses comics cannot support the new comics format 2020-08-10 21:15:08 -04:00
Jobobby04 eee2c34abf Fix library actions overflow menu 2020-08-10 18:30:10 -04:00
Jobobby04 c272eb6059 Move the Realm init out of the globalscope in hope it fixes crashes 2020-08-10 14:45:18 -04:00
Jobobby04 74065afc27 Hopefully fix issues with certain actions for some people 2020-08-10 00:55:39 -04:00
Jobobby04 ead5a258be More drag protection 2020-08-10 00:54:56 -04:00
Jobobby04 f9cf017594 Set smart reader background as the default 2020-08-09 20:22:26 -04:00
Mike 1211b2c86a Fix Hitomi chapters (#75) 2020-08-09 20:00:49 -04:00
Jobobby04 2bc845b1d2 Simple way to get chapter date for nHentai 2020-08-09 19:44:17 -04:00
Jobobby04 da8ed5c74f Fix zoom out webtoon showing in pager mode 2020-08-09 19:42:03 -04:00
arkon d7d1d97f5f Add option to prevent deleting bookmarked chapters (closes #2082)
(cherry picked from commit dbaac69fad)
2020-08-09 19:20:01 -04:00
arkon 63510b2e60 Minor cleanup
(cherry picked from commit b6a1e89535)
2020-08-09 19:19:53 -04:00
arkon d7976e6054 Minor rewording of chapter deletion settings
(cherry picked from commit cce919750a)
2020-08-09 19:19:45 -04:00
arkon c82d7db570 Bubble up sources with results in global search (closes #3598)
(cherry picked from commit 9376b223bb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt
2020-08-09 19:19:32 -04:00
arkon b6f8db81ee Update OkHttp
(cherry picked from commit 6f047fb5aa)
2020-08-09 19:18:14 -04:00
arkon a2fb89066c Swallow errors when trying to determine available disk space when downloading (closes #3603)
(cherry picked from commit 3e6b0117fd)
2020-08-09 19:18:05 -04:00
arkon 6f71bb3abe Allow partially loading extensions with individually marked NSFW sources
(cherry picked from commit 421dfb4a2d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPresenter.kt
2020-08-09 19:17:55 -04:00
Jobobby04 e945de74f2 Add manga grouping in library!! This was inspired by J2ks version of the feature 2020-08-09 19:13:47 -04:00
Jobobby04 0e43234c23 More cleanup for drag and drop 2020-08-08 17:09:30 -04:00
arkon f8c4bbdfd8 Option to hide NSFW extensions (closes #1312) (SY will expand more on this when preview finishes it)
(cherry picked from commit abaca6e676)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionPresenter.kt
2020-08-08 16:47:29 -04:00
Jobobby04 c834c2fbb0 Tweak dragging so it works in action mode if there is only 1 manga selected 2020-08-08 16:32:03 -04:00
arkon ad62a6b10b Add mark as read/unread to library (closes #156)
Adapted from https://github.com/CarlosEsco/Neko/commit/e51276a1acb233467bbb1c7214be0ec105c62cb7

(cherry picked from commit 8bab1d9798)
2020-08-08 16:07:34 -04:00
arkon 04fbb70981 Explicitly depend on core-ktx
(cherry picked from commit 13d31669ac)
2020-08-08 16:07:25 -04:00
Jobobby04 340e534ca9 Remove uneeded checks for should move, fixes moving a manga to the first positon 2020-08-08 16:07:18 -04:00
Jobobby04 5714f183a8 Update drag and drop to work like J2k's new version
Cleanup some stuff from the continue reading button
2020-08-07 23:34:58 -04:00
Jobobby04 b6f6607d91 Cleanup readme 2020-08-06 21:27:10 -04:00
arkon aa794f4703 Fix MAL 0/10 scores (closes #3623)
(cherry picked from commit c1dfdeb500)
2020-08-06 21:21:43 -04:00
arkon a6d6a0fca6 Dismiss add manga snackbar when leaving controller (closes #3614)
(cherry picked from commit dda7e677a5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2020-08-06 21:21:34 -04:00
Jobobby04 aa6f1a0de5 Cleanup 2020-08-06 21:09:25 -04:00
Jobobby04 52b9a1dbd2 Fix resources being in the wrong spot for some reason 2020-08-06 18:32:29 -04:00
Jobobby04 2f98dd2046 Add continue reading button, some resources gotten from J2k 2020-08-06 17:45:18 -04:00
Jobobby04 f60b29c763 Replace cleanup downloads for loops with forEach loops 2020-08-06 16:15:40 -04:00
Jobobby04 c2adf2fe0a Copy smart background from J2k 2020-08-06 16:11:00 -04:00
Jobobby04 c340884adb Copy enable/disable zoom out in webtoon reader from J2k 2020-08-06 14:59:58 -04:00
Jobobby04 0125f326b4 Copy cleanup orphaned downloads from J2k 2020-08-06 14:59:31 -04:00
Jobobby04 2de87f8d29 Cleanup 2020-08-06 14:57:54 -04:00
arkon 165b243aab Warn before restoring backup if trackers aren't logged in
(cherry picked from commit b1fb401f63)
2020-08-05 22:56:46 -04:00
arkon 961f7f9b6d Fix toolbar being expanded when opening preference dialogs
(cherry picked from commit 885ace111e)
2020-08-05 22:56:35 -04:00
Jobobby04 61094edeed Cleanup some code 2020-08-05 22:46:04 -04:00
Jobobby04 fbe4f6ad62 Dont download new chapters after refreshing a manga if its from E/ExHentai 2020-08-05 20:46:14 -04:00
Jobobby04 c552934acc Fix quick clean nHentai manga 2020-08-05 18:26:02 -04:00
Jobobby04 44c9df8c9b Add quick clean E-Hentai/nHentai titles, select them in your library and you can quick clean them 2020-08-05 18:23:36 -04:00
jobobby04 594a02fa69 Fix release builder 2020-08-05 15:01:24 -04:00
Jobobby04 510a67a755 Finishing a E-Hentai chapter will now mark previous chapters as read 2020-08-05 14:28:40 -04:00
Jobobby04 882856a028 Lint 2020-08-05 13:32:28 -04:00
Jobobby04 76adeae5ed Fix bug when pressing download unread chapters on a E-Hentai manga from library will download the latest chapter even if its read 2020-08-05 13:32:11 -04:00
arkon eb3c9a1d58 Move tracker setting dialogs
(cherry picked from commit 885552b792)
2020-08-04 23:35:31 -04:00
arkon 29f74ba423 Minor cleanup
(cherry picked from commit 4f02872a84)
2020-08-04 23:35:22 -04:00
arkon 571778adc1 Revert "Use insetter library for handling inset padding" (fixes #3586)
This reverts commit 3ddd1033c3.

(cherry picked from commit ecec1bd102)
2020-08-04 23:35:12 -04:00
Jobobby04 64c5b70c78 Redo the EH library search engine, make it work for every manga, meaning exclusion, partial matching, and a bunch of other things now working the library search 2020-08-04 23:34:26 -04:00
Jobobby04 bb87392eef Code cleanup 2020-08-04 22:51:56 -04:00
Jobobby04 29e1697d2e Fix migration crash because fetch chapter list through a exception 2020-08-03 19:44:02 -04:00
arkon 025d794962 Fix snackbars not being in viewport properly
(cherry picked from commit 060f0682f4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2020-08-03 18:25:27 -04:00
arkon a284f5cd08 Use dialog to show what's new release info
(cherry picked from commit 88032e11df)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2020-08-03 18:24:11 -04:00
arkon ee6b536b94 Adjust vertical reading mode tap zones (closes #3551)
Basically L shapes, where top/left goes back, bottom/right goes forward, and middle opens the menu.

(cherry picked from commit 493c8b0943)
2020-08-03 18:14:22 -04:00
arkon 285f65ca4f Remove Tagalog translations (closes #3579)
(cherry picked from commit af2ef0621a)
2020-08-03 18:14:10 -04:00
arkon d07dbee9b0 Explicitly dismiss progress notification on downloader stop
(cherry picked from commit 095461e31b)
2020-08-03 18:14:02 -04:00
arkon a5e1f92b05 Use insetter library for handling inset padding
(cherry picked from commit 3ddd1033c3)
2020-08-03 18:13:53 -04:00
arkon 417a31cfad Adjust download badge color again
(cherry picked from commit 912687ac78)
2020-08-03 18:13:45 -04:00
arkon a84df3501a Request gzipped version of extensions repo
(cherry picked from commit 40a9595012)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
2020-08-03 18:13:38 -04:00
arkon 01137bf476 Fix manga title disappearing in toolbar when pushing another controller
(cherry picked from commit 12ff37d052)
2020-08-03 18:08:33 -04:00
arkon 4c20ba38cb Revert "Use AndroidX WebKit library"
This reverts commit 7e7eb9f39f.

(cherry picked from commit 4857073f30)

# Conflicts:
#	app/build.gradle
2020-08-03 18:08:24 -04:00
Jobobby04 cb4daa81c4 Add a release builder action 2020-08-03 17:33:07 -04:00
Jobobby04 4f803494ff Update the EH search engine to fix issues with the current search features 2020-08-03 17:21:10 -04:00
Jobobby04 fb19f6b860 Update nHentai internal logic to be the same as the extension 2020-08-03 12:54:46 -04:00
jobobby04 885c94f9c8 Make the preview builder only happen on master 2020-08-03 12:47:16 -04:00
joseph619 a5b7ad6495 Update preview images to 1.1.0 2020-08-02 21:02:49 -04:00
207 changed files with 3490 additions and 2822 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.1.1) - I have updated to the latest version of the app (stable is v1.2.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -10,7 +10,7 @@ I acknowledge that:
--- ---
### Device information ## Device information
* Tachiyomi version: ? * Tachiyomi version: ?
* Android version: ? * Android version: ?
* Device: ? * Device: ?
+3 -3
View File
@@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.1.1) - I have updated to the latest version of the app (stable is v1.2.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -17,7 +17,7 @@ I acknowledge that:
--- ---
### Device information ## Device information
* Tachiyomi version: ? * Tachiyomi version: ?
* Android version: ? * Android version: ?
* Device: ? * Device: ?
@@ -32,5 +32,5 @@ This should happen.
### Actual behavior ### Actual behavior
This happened instead. This happened instead.
### Other details ## Other details
Additional details and attachments. Additional details and attachments.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Tachiyomi help website
url: https://tachiyomi.org/help/
about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository
url: https://github.com/inorichi/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead.
+3 -3
View File
@@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.1.1) - I have updated to the latest version of the app (stable is v1.2.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -17,8 +17,8 @@ I acknowledge that:
--- ---
### Why/User Benefit/User Problem ## Why/User Benefit/User Problem
(explain why this feature should be added) (explain why this feature should be added)
### What/Requirements ## What/Requirements
(explain how this feature would behave) (explain how this feature would behave)
+2 -2
View File
@@ -2,7 +2,7 @@
name: "Extension/source/catalogue issue" name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions" about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions" title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
labels: "catalog" labels: "catalog, invalid"
--- ---
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 482 KiB

+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="svg8" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 172 172" style="enable-background:new 0 0 172 172;" xml:space="preserve">
<style type="text/css">
.st0{stroke:#CE2828;stroke-width:14;stroke-linecap:round;stroke-linejoin:round;}
.st1{fill:#F7D009;}
.st2{fill:#E40F85;}
</style>
<title>sy_hobo_stds_mine</title>
<g id="layer1">
<path id="path4535" class="st0" d="M85.3,7C129,6.6,164.6,41.7,165,85.3c0.4,43.6-34.7,79.3-78.3,79.7C43.1,165.4,7.4,130.3,7,86.7
c0-0.5,0-0.9,0-1.4C7.4,42.2,42.2,7.4,85.3,7z"/>
<g id="text4543">
<path id="path4545" class="st1" d="M76,64.2c2.9,0,8.4-9.4,8.4-12.5S73.5,40.7,58.2,40.7c-21.4,0-27.4,15-27.4,23.8
c0,9.1,2.5,19.6,25.6,26.6c6.1,2,15.6,5.1,15.6,12.8c0,6.5-6.9,9.9-15,9.9c-22.6,0-20.9-21.3-22.6-21.3c-1.1,0-6.4,5.1-6.4,14.2
c0,16.7,15.2,24.7,30.1,24.7c22.3,0,31-15,31-27.2c0-9.9-4.5-20.7-26.7-28.1c-5.8-2-16.2-4.8-16.2-12.5c0-6.2,6.8-8.8,12-8.8
C69.2,54.8,73.3,64.2,76,64.2L76,64.2z"/>
<path id="path4547" class="st2" d="M95.4,128.7c0,1.4,1.1,2.6,2.6,2.6c23.2,0,47-29.8,46-60.7c0-4.5,0.3-7.9-1.7-8.2h-9.4
c-1.2,0-3.8-0.3-3.8,1.4s1.2,6.2,1.2,11.3c0,8.2-2.8,21-7.1,21c-2.1,0-12.4-11.6-12.4-24.1c0-3.1,1-6.2,1-7.9c0-2-2.3-1.7-3.7-1.7
h-8.6c-4.1,0-4,0-4,4.8c0,29.5,18.3,36.8,18.3,41.4c0,1.1-3.1,5.1-15.3,5.1c-2.8,0-3.1,3.1-3.1,4.3L95.4,128.7z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -2,6 +2,8 @@ name: Remote Dispatch Action Initiator
on: on:
push: push:
branches:
- 'master'
repository_dispatch: repository_dispatch:
jobs: jobs:
@@ -0,0 +1,67 @@
name: Release Builder
on:
push:
branches:
- 'release'
jobs:
apk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Get NDK
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.0
with:
# The path to the file to write
path: app/google-services.json
# The contents of the file
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
write-mode: overwrite # optional, default is preserve
- name: Build Release APK
run: bash ./gradlew assembleRelease --stacktrace
- name: Sign Android Release
uses: r0adkll/sign-android-release@v1
with:
# The directory to find your release to sign
releaseDirectory: app/build/outputs/apk/standard/release
# The key used to sign your release in base64 encoded format
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
# The key alias
alias: ${{ secrets.ALIAS }}
# The password to the keystore
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
# The password for the key
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.run_number }}
release_name: TachiyomiSY
draft: true
prerelease: false
- name: Upload Release APK
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
asset_name: TachiyomiSY.apk
asset_content_type: application/vnd.android.package-archive
+14 -9
View File
@@ -1,6 +1,6 @@
| Preview Builds | Release Builds | Tachiyomi Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) | | [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
Features of TachiyomiSY include: Features of TachiyomiSY include:
* Uses the new Tachiyomi Stable UI * Uses the new Tachiyomi Stable UI
* Custom manga page, all your needs, such as info and chapters, in front of your face
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab * Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
* Hentai features enable/disable, in advanced settings * Hentai features enable/disable, in advanced settings
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one * Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
@@ -37,20 +36,25 @@ Features of TachiyomiSY include:
* Manga info edit * Manga info edit
* Enhanced views for internal and integrated sources * Enhanced views for internal and integrated sources
* Enhanced usability for internal and delegated sources * Enhanced usability for internal and delegated sources
* Dynamic Categories, view the library in multiple ways
* Smart background for reading modes like LTR or Vertical, changes the backgorund based on the page color
* Force disable webtoon zoom
* Continue reading button in library
* Quick clean titles
Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
* Source migration, migrate all your manga from one source to another * Source migration, migrate all your manga from one source to another
* Custom hentai sources: * Custom hentai sources:
* * E-Hentai/ExHentai * * E-Hentai/ExHentai
* * nHentai
* * Hitomi.la
* * 8Muses
* * Perv Eden
* Additional features for some extensions, features include custom description, opening in app, batch add to library: * Additional features for some extensions, features include custom description, opening in app, batch add to library:
* * 8Muses (EroMuse)
* * HBrowse
* * HentaiCafe (Foolside)
* * Hitomi.la
* * NHentai
* * PervEden (EN and IT)
* * Puruin * * Puruin
* * Tsumino * * Tsumino
* * HentaiCafe (Foolside)
* * HBrowse
* Saving searches * Saving searches
* Autoscroll * Autoscroll
* Page preload customization * Page preload customization
@@ -64,10 +68,11 @@ Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified
* Click tag for local search, long click tag for global search * Click tag for local search, long click tag for global search
* Merge multiple of the same manga from different sources * Merge multiple of the same manga from different sources
* Drag and drop library sorting * Drag and drop library sorting
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
## Download ## Download
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases). Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases). If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
+14 -15
View File
@@ -42,8 +42,8 @@ android {
minSdkVersion AndroidConfig.minSdk minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 4 versionCode 6
versionName "1.1.1" versionName "1.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -146,19 +146,19 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'androidx.webkit:webkit:1.3.0-rc01'
final lifecycle_version = '2.3.0-alpha05' final lifecycle_version = '2.3.0-alpha06'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Job scheduling // Job scheduling
final work_version = '2.4.0-rc01' final work_version = '2.4.0'
implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version"
@@ -174,11 +174,11 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0' implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client // Network client
final okhttp_version = '4.7.2' final okhttp_version = '4.8.1'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version" implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
implementation 'com.squareup.okio:okio:2.6.0' implementation 'com.squareup.okio:okio:2.7.0'
// TLS 1.3 support for Android < 10 // TLS 1.3 support for Android < 10
implementation 'org.conscrypt:conscrypt-android:2.4.0' implementation 'org.conscrypt:conscrypt-android:2.4.0'
@@ -214,7 +214,7 @@ dependencies {
implementation 'androidx.sqlite:sqlite:2.1.0' implementation 'androidx.sqlite:sqlite:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.31.0' implementation 'io.requery:sqlite-android:3.32.2'
// Preferences // Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0' implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
@@ -239,8 +239,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports // Crash reports
//final acra_version = '5.5.0' //implementation 'ch.acra:acra-http:5.7.0'
//implementation "ch.acra:acra-http:$acra_version"
// Sort // Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
@@ -278,7 +277,7 @@ dependencies {
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Licenses // Licenses
final aboutlibraries_version = '8.2.0' final aboutlibraries_version = '8.3.0'
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version" implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version" implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
@@ -303,10 +302,10 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
// Debug tool; see https://fbflipper.com/ // Debug tool; see https://fbflipper.com/
// debugImplementation 'com.facebook.flipper:flipper:0.49.0' // debugImplementation 'com.facebook.flipper:flipper:0.50.0'
// debugImplementation 'com.facebook.soloader:soloader:0.9.0' // debugImplementation 'com.facebook.soloader:soloader:0.9.0'
// Text distance (EH) // Text distance (EH)
+8
View File
@@ -37,6 +37,14 @@
public *; public *;
} }
# Hitomi extension crash fix
-keepclassmembers class rx.Single {
*** onSubscribe;
final *;
protected *;
public *;
}
# RxJava 1.1.0 # RxJava 1.1.0
-dontwarn sun.misc.** -dontwarn sun.misc.**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+1 -1
View File
@@ -77,6 +77,7 @@ open class App : Application(), LifecycleObserver {
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
setupNotificationChannels() setupNotificationChannels()
Realm.init(this)
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH) GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
// Reprint.initialize(this) //Setup fingerprint (EH) // Reprint.initialize(this) //Setup fingerprint (EH)
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
@@ -133,7 +134,6 @@ open class App : Application(), LifecycleObserver {
// EXH // EXH
private fun deleteOldMetadataRealm() { private fun deleteOldMetadataRealm() {
Realm.init(this)
val config = RealmConfiguration.Builder() val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm") .name("gallery-metadata.realm")
.schemaVersion(3) .schemaVersion(3)
@@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.annoations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.injectLazy
object BackupRestoreValidator { object BackupRestoreValidator {
private val sourceManager: SourceManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
/** /**
* Checks for critical backup file data. * Checks for critical backup file data.
* *
* @throws Exception if version or manga cannot be found. * @throws Exception if version or manga cannot be found.
* @return List of required sources. * @return List of missing sources or missing trackers.
*/ */
fun validate(context: Context, uri: Uri): Map<Long, String> { fun validate(context: Context, uri: Uri): Results {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject val json = JsonParser.parseReader(reader).asJsonObject
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data)) throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
} }
if (mangasJson.asJsonArray.size() == 0) { val mangas = mangasJson.asJsonArray
if (mangas.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
} }
return getSourceMapping(json) val sources = getSourceMapping(json)
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.values
.sorted()
val trackers = mangas
.filter { it.asJsonObject.has("track") }
.flatMap { it.asJsonObject["track"].asJsonArray }
.map { it.asJsonObject["s"].asInt }
.distinct()
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.sorted()
return Results(missingSources, missingTrackers)
} }
fun getSourceMapping(json: JsonObject): Map<Long, String> { fun getSourceMapping(json: JsonObject): Map<Long, String> {
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
} }
.toMap() .toMap()
} }
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
} }
@@ -32,6 +32,12 @@ interface Manga : SManga {
return genre?.split(", ")?.map { it.trim() } return genre?.split(", ")?.map { it.trim() }
} }
// SY -->
fun getOriginalGenres(): List<String>? {
return originalGenre?.split(", ")?.map { it.trim() }
}
// SY <--
private fun setFlags(flag: Int, mask: Int) { private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask) chapter_flags = chapter_flags and mask.inv() or (flag and mask)
} }
@@ -200,6 +200,18 @@ class DownloadCache(
} }
} }
// SY -->
fun removeFolders(folders: List<String>, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
folders.forEach { chapter ->
if (chapter in mangaDir.files) {
mangaDir.files -= chapter
}
}
}
// SY <--
/** /**
* Removes a list of chapters that have been deleted from this cache. * Removes a list of chapters that have been deleted from this cache.
* *
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -24,10 +25,8 @@ import uy.kohesive.injekt.injectLazy
*/ */
class DownloadManager(/* SY private */ val context: Context) { class DownloadManager(/* SY private */ val context: Context) {
/** private val sourceManager: SourceManager by injectLazy()
* The sources manager. private val preferences: PreferencesHelper by injectLazy()
*/
private val sourceManager by injectLazy<SourceManager>()
/** /**
* Downloads provider, used to retrieve the folders where the chapters are or should be stored. * Downloads provider, used to retrieve the folders where the chapters are or should be stored.
@@ -201,14 +200,47 @@ class DownloadManager(/* SY private */ val context: Context) {
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
queue.remove(chapters) queue.remove(chapters)
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
chapters.filterNot { it.bookmark }
} else {
chapters
}
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() } chapterDirs.forEach { it.delete() }
cache.removeChapters(chapters, manga) cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete() chapterDirs.firstOrNull()?.parentFile?.delete()
} }
} }
// SY -->
/**
* Deletes the directories of chapters that were read or have no match
*
* @param chapters the list of chapters to delete.
* @param manga the manga of the chapters.
* @param source the source of the chapters.
*/
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int {
var cleaned = 0
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
cleaned += filesWithNoChapter.size
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
filesWithNoChapter.forEach { it.delete() }
val readChapters = allChapters.filter { it.read }
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
readChapterDirs.forEach { it.delete() }
cleaned += readChapterDirs.size
cache.removeChapters(readChapters, manga)
if (cache.getDownloadCount(manga) == 0) {
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
}
return cleaned
}
// SY <--
/** /**
* Deletes the directory of a downloaded manga. * Deletes the directory of a downloaded manga.
* *
@@ -79,7 +79,7 @@ internal class DownloadNotifier(private val context: Context) {
* Dismiss the downloader's notification. Downloader error notifications use a different id, so * Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user. * those can only be dismissed by the user.
*/ */
fun dismiss() { fun dismissProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS) context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
} }
@@ -109,6 +109,32 @@ class DownloadProvider(private val context: Context) {
} }
} }
// SY -->
/**
* Returns a list of all files in manga directory
*
* @param chapters the chapters to query.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findUnmatchedChapterDirs(
chapters: List<Chapter>,
manga: Manga,
source: Source
): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles()!!.asList().filter {
(
chapters.find { chp ->
getValidChapterDirNames(chp).any { dir ->
mangaDir.findFile(dir) != null
}
} == null
) || it.name?.endsWith("_tmp") == true
}
}
// SY <--
/** /**
* Returns the download directory name for a source. * Returns the download directory name for a source.
* *
@@ -139,6 +139,7 @@ class Downloader(
notifier.paused = false notifier.paused = false
notifier.onPaused() notifier.onPaused()
} else { } else {
notifier.dismissProgress()
notifier.onComplete() notifier.onComplete()
} }
} }
@@ -170,7 +171,7 @@ class Downloader(
.forEach { it.status = Download.NOT_DOWNLOADED } .forEach { it.status = Download.NOT_DOWNLOADED }
} }
queue.clear() queue.clear()
notifier.dismiss() notifier.dismissProgress()
} }
/** /**
@@ -266,15 +267,16 @@ class Downloader(
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) val mangaDir = provider.getMangaDir(download.manga, download.source)
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) { val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.ERROR download.status = Download.ERROR
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name) notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
return@defer Observable.just(download) return@defer Observable.just(download)
} }
val chapterDirname = provider.getChapterDirName(download.chapter)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX) val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
@@ -155,7 +155,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga * @param mangaId id of manga
* @param chapterId id of chapter * @param chapterId id of chapter
*/ */
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) { private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
val db = DatabaseHelper(context) val db = DatabaseHelper(context)
val manga = db.getManga(mangaId).executeAsBlocking() val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking()
@@ -97,6 +97,8 @@ object PreferenceKeys {
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key" const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
const val removeBookmarkedChapters = "pref_remove_bookmarked"
const val libraryUpdateInterval = "pref_library_update_interval_key" const val libraryUpdateInterval = "pref_library_update_interval_key"
const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateRestriction = "library_update_restriction"
@@ -121,6 +123,8 @@ object PreferenceKeys {
const val automaticExtUpdates = "automatic_ext_updates" const val automaticExtUpdates = "automatic_ext_updates"
const val allowNsfwSource = "allow_nsfw_source"
const val startScreen = "start_screen" const val startScreen = "start_screen"
const val useBiometricLock = "use_biometric_lock" const val useBiometricLock = "use_biometric_lock"
@@ -183,8 +187,6 @@ object PreferenceKeys {
const val eh_lock_manually = "eh_lock_manually" const val eh_lock_manually = "eh_lock_manually"
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
const val eh_showSyncIntro = "eh_show_sync_intro" const val eh_showSyncIntro = "eh_show_sync_intro"
const val eh_readOnlySync = "eh_sync_read_only" const val eh_readOnlySync = "eh_sync_read_only"
@@ -237,8 +239,6 @@ object PreferenceKeys {
const val eh_aggressivePageLoading = "eh_aggressive_page_loading" const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
const val eh_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
const val eh_preload_size = "eh_preload_size" const val eh_preload_size = "eh_preload_size"
const val eh_tag_filtering_value = "eh_tag_filtering_value" const val eh_tag_filtering_value = "eh_tag_filtering_value"
@@ -273,7 +273,11 @@ object PreferenceKeys {
const val recommendsInOverflow = "recommends_in_overflow" const val recommendsInOverflow = "recommends_in_overflow"
const val hitomiAlwaysWebp = "hitomi_always_webp"
const val enhancedEHentaiView = "enhanced_e_hentai_view" const val enhancedEHentaiView = "enhanced_e_hentai_view"
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
const val startReadingButton = "start_reading_button"
const val groupLibraryBy = "group_library_by"
} }
@@ -37,4 +37,10 @@ object PreferenceValues {
VERTICAL, VERTICAL,
BOTH BOTH
} }
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
}
} }
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import java.io.File import java.io.File
@@ -113,7 +114,7 @@ class PreferencesHelper(val context: Context) {
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1) fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1) fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true) fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
@@ -187,6 +188,8 @@ class PreferencesHelper(val context: Context) {
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false) fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24) fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
@@ -222,6 +225,8 @@ class PreferencesHelper(val context: Context) {
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true) fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0) fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0) fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
@@ -307,8 +312,6 @@ class PreferencesHelper(val context: Context) {
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "") fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "") fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
fun eh_nh_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true) fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false) fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
@@ -351,8 +354,6 @@ class PreferencesHelper(val context: Context) {
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false) fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
fun eh_hl_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4) fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true) fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
@@ -377,7 +378,11 @@ class PreferencesHelper(val context: Context) {
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false) fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
fun hitomiAlwaysWebp() = flowPrefs.getBoolean(Keys.hitomiAlwaysWebp, true)
fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true) fun enhancedEHentaiView() = flowPrefs.getBoolean(Keys.enhancedEHentaiView, true)
fun webtoonEnableZoomOut() = flowPrefs.getBoolean(Keys.webtoonEnableZoomOut, false)
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
} }
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.preference package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() { class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
@@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
prefs.edit().putBoolean(key, value).apply() prefs.edit {
putBoolean(key, value)
}
} }
override fun getInt(key: String?, defValue: Int): Int { override fun getInt(key: String?, defValue: Int): Int {
@@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putInt(key: String?, value: Int) { override fun putInt(key: String?, value: Int) {
prefs.edit().putInt(key, value).apply() prefs.edit {
putInt(key, value)
}
} }
override fun getLong(key: String?, defValue: Long): Long { override fun getLong(key: String?, defValue: Long): Long {
@@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putLong(key: String?, value: Long) { override fun putLong(key: String?, value: Long) {
prefs.edit().putLong(key, value).apply() prefs.edit {
putLong(key, value)
}
} }
override fun getFloat(key: String?, defValue: Float): Float { override fun getFloat(key: String?, defValue: Float): Float {
@@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putFloat(key: String?, value: Float) { override fun putFloat(key: String?, value: Float) {
prefs.edit().putFloat(key, value).apply() prefs.edit {
putFloat(key, value)
}
} }
override fun getString(key: String?, defValue: String?): String? { override fun getString(key: String?, defValue: String?): String? {
@@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putString(key: String?, value: String?) { override fun putString(key: String?, value: String?) {
prefs.edit().putString(key, value).apply() prefs.edit {
putString(key, value)
}
} }
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? { override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
@@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putStringSet(key: String?, values: MutableSet<String>?) { override fun putStringSet(key: String?, values: MutableSet<String>?) {
prefs.edit().putStringSet(key, values).apply() prefs.edit {
putStringSet(key, values)
}
} }
} }
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
fun copyPersonalFrom(track: Track) { fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString() num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt() val numScore = track.score.toInt()
if (numScore in 1..9) { if (numScore == 0) {
score = ""
} else if (numScore in 1..10) {
score = numScore.toString() score = numScore.toString()
} }
status = track.status.toString() status = track.status.toString()
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
override suspend fun checkForUpdate(): UpdateResult { override suspend fun checkForUpdate(): UpdateResult {
val response = withContext(Dispatchers.IO) { val response = withContext(Dispatchers.IO) {
client.newCall(GET(DevRepoRelease.LATEST_URL)).await(assertSuccess = false) client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
} }
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk" // Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
@@ -19,13 +19,8 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HITOMI_SOURCE_ID
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.NHENTAI_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import kotlinx.coroutines.async import kotlinx.coroutines.async
import rx.Observable import rx.Observable
@@ -83,11 +78,6 @@ class ExtensionManager(
return when (source.id) { return when (source.id) {
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
PERV_EDEN_EN_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
PERV_EDEN_IT_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source) MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
else -> null else -> null
} }
@@ -1,44 +1,30 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import java.util.Date import java.util.Date
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
private val network: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val gson: Gson by injectLazy()
suspend fun findExtensions(): List<Extension.Available> { suspend fun findExtensions(): List<Extension.Available> {
val call = GET(EXT_URL) val service: ExtensionGithubService = ExtensionGithubService.create()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val response = network.client.newCall(call).await() val response = service.getRepo()
if (response.isSuccessful) { parseResponse(response)
parseResponse(response)
} else {
response.close()
throw Exception("Failed to get extensions")
}
} }
} }
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun parseResponse(response: Response): List<Extension.Available> { private fun parseResponse(json: JsonArray): List<Extension.Available> {
val text = response.body?.use { it.string() } ?: return emptyList()
val json = gson.fromJson<JsonArray>(text)
return json return json
.filter { element -> .filter { element ->
val versionName = element["version"].string val versionName = element["version"].string
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
val versionName = element["version"].string val versionName = element["version"].string
val versionCode = element["code"].int val versionCode = element["code"].int
val lang = element["lang"].string val lang = element["lang"].string
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}" val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL/apk/${extension.apkName}" return "$REPO_URL_PREFIX/apk/${extension.apkName}"
} }
// SY --> // SY -->
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
// SY <-- // SY <--
companion object { companion object {
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo" const val BASE_URL = "https://raw.githubusercontent.com/"
private const val EXT_URL = "$REPO_URL/index.json" const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
} }
} }
@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.extension.api
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.network.NetworkHelper
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz")
suspend fun getRepo(): JsonArray
}
@@ -9,14 +9,16 @@ sealed class Extension {
abstract val versionName: String abstract val versionName: String
abstract val versionCode: Int abstract val versionCode: Int
abstract val lang: String? abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed( data class Installed(
override val name: String, override val name: String,
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val sources: List<Source>,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false, val hasUpdate: Boolean = false,
val isObsolete: Boolean = false, val isObsolete: Boolean = false,
val isUnofficial: Boolean = false, val isUnofficial: Boolean = false,
@@ -31,6 +33,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String val iconUrl: String
) : Extension() ) : Extension()
@@ -41,6 +44,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val signatureHash: String, val signatureHash: String,
override val lang: String? = null override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension() ) : Extension()
} }
@@ -5,6 +5,8 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
/** /**
* Class that handles the loading of the extensions installed in the system. * Class that handles the loading of the extensions installed in the system.
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader { internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy()
private val allowNsfwSource by lazy {
preferences.allowNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension" private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2 const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2 const val LIB_VERSION_MAX = 1.2
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key // inorichi's key
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
return LoadResult.Untrusted(extension) return LoadResult.Untrusted(extension)
} }
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed")
}
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
try { try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) { when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is Source -> listOf(obj) is Source -> listOf(obj)
is SourceFactory -> obj.createSources() is SourceFactory -> {
if (isSourceNsfw(obj)) {
emptyList()
} else {
obj.createSources()
}
}
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
.filter { !isSourceNsfw(it) }
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang } .map { it.lang }
.toSet() .toSet()
val lang = when (langs.size) { val lang = when (langs.size) {
0 -> "" 0 -> ""
1 -> langs.first() 1 -> langs.first()
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
} }
val extension = Extension.Installed( val extension = Extension.Installed(
extName, pkgName, versionName, versionCode, sources, lang, extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
isUnofficial = signatureHash != officialSignature isUnofficial = signatureHash != officialSignature
) )
return LoadResult.Success(extension) return LoadResult.Success(extension)
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
null null
} }
} }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
return false
}
if (clazz !is Source && clazz !is SourceFactory) {
return false
}
// Annotations are proxied, hence this janky way of checking for them
return clazz.javaClass.annotations
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
.firstOrNull { it == Nsfw::class.java.simpleName } != null
}
} }
@@ -2,18 +2,16 @@ package eu.kanade.tachiyomi.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
@@ -116,7 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
// HTTP error codes are only received since M // HTTP error codes are only received since M
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
url == origRequestUrl && !challengeFound url == origRequestUrl && !challengeFound
) { ) {
// The first request didn't return the challenge, abort. // The first request didn't return the challenge, abort.
@@ -124,13 +122,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
} }
override fun onReceivedHttpError( override fun onReceivedErrorCompat(
view: WebView, view: WebView,
request: WebResourceRequest, errorCode: Int,
errorResponse: WebResourceResponse description: String?,
failingUrl: String,
isMainFrame: Boolean
) { ) {
if (request.isForMainFrame) { if (isMainFrame) {
if (errorResponse.statusCode == 503) { if (errorCode == 503) {
// Found the Cloudflare challenge page. // Found the Cloudflare challenge page.
challengeFound = true challengeFound = true
} else { } else {
@@ -20,10 +20,14 @@ import eu.kanade.tachiyomi.source.online.english.HentaiCafe
import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HBROWSE_SOURCE_ID
import exh.HENTAI_CAFE_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID import exh.PERV_EDEN_IT_SOURCE_ID
import exh.metadata.metadata.PervEdenLang import exh.PURURIN_SOURCE_ID
import exh.TSUMINO_SOURCE_ID
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
@@ -104,7 +108,7 @@ open class SourceManager(private val context: Context) {
source, source,
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context) delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
) )
val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.originalSourceQualifiedClassName } val map = listOf(DelegatedSource(enhancedSource.originalSource.name, enhancedSource.originalSource.id, enhancedSource.originalSource::class.qualifiedName ?: delegate.originalSourceQualifiedClassName, (enhancedSource.enhancedSource as DelegatedHttpSource)::class, delegate.factory)).associateBy { it.sourceId }
currentDelegatedSources.plusAssign(map) currentDelegatedSources.plusAssign(map)
enhancedSource enhancedSource
} else source } else source
@@ -136,11 +140,6 @@ open class SourceManager(private val context: Context) {
if (prefs.enableExhentai().get()) { if (prefs.enableExhentai().get()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context) exSrcs += EHentai(EXH_SOURCE_ID, true, context)
} }
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en, context)
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it, context)
exSrcs += NHentai(context)
exSrcs += Hitomi(context)
exSrcs += EightMuses(context)
return exSrcs return exSrcs
} }
// SY <-- // SY <--
@@ -173,23 +172,23 @@ open class SourceManager(private val context: Context) {
// SY --> // SY -->
companion object { companion object {
private const val fillInSourceId = 9999L private const val fillInSourceId = Long.MAX_VALUE
val DELEGATED_SOURCES = listOf( val DELEGATED_SOURCES = listOf(
DelegatedSource( DelegatedSource(
"Hentai Cafe", "Hentai Cafe",
260868874183818481, HENTAI_CAFE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe", "eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
HentaiCafe::class HentaiCafe::class
), ),
DelegatedSource( DelegatedSource(
"Pururin", "Pururin",
2221515250486218861, PURURIN_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.pururin.Pururin", "eu.kanade.tachiyomi.extension.en.pururin.Pururin",
Pururin::class Pururin::class
), ),
DelegatedSource( DelegatedSource(
"Tsumino", "Tsumino",
6707338697138388238, TSUMINO_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class Tsumino::class
)/*, )/*,
@@ -202,13 +201,45 @@ open class SourceManager(private val context: Context) {
)*/, )*/,
DelegatedSource( DelegatedSource(
"HBrowse", "HBrowse",
1401584337232758222, HBROWSE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse", "eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
HBrowse::class HBrowse::class
),
DelegatedSource(
"8Muses",
EIGHTMUSES_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse",
EightMuses::class
),
DelegatedSource(
"Hitomi",
fillInSourceId,
"eu.kanade.tachiyomi.extension.all.hitomi.Hitomi",
Hitomi::class,
true
),
DelegatedSource(
"PervEden English",
PERV_EDEN_EN_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.perveden.Perveden",
PervEden::class
),
DelegatedSource(
"PervEden Italian",
PERV_EDEN_IT_SOURCE_ID,
"eu.kanade.tachiyomi.extension.it.perveden.Perveden",
PervEden::class
),
DelegatedSource(
"NHentai",
fillInSourceId,
"eu.kanade.tachiyomi.extension.all.nhentai.NHentai",
NHentai::class,
true
) )
).associateBy { it.originalSourceQualifiedClassName } ).associateBy { it.originalSourceQualifiedClassName }
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>() var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
data class DelegatedSource( data class DelegatedSource(
val sourceName: String, val sourceName: String,
@@ -3,100 +3,49 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.HITOMI_SOURCE_ID
import exh.hitomi.HitomiNozomi
import exh.metadata.metadata.HitomiSearchMetadata import exh.metadata.metadata.HitomiSearchMetadata
import exh.metadata.metadata.HitomiSearchMetadata.Companion.BASE_URL import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HitomiDescriptionAdapter import exh.ui.metadata.adapters.HitomiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.vepta.vdm.ByteCursor
import rx.Observable import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
/** class Hitomi(delegate: HttpSource, val context: Context) :
* Man, I hate this source :( DelegatedHttpSource(delegate),
*/ LewdSource<HitomiSearchMetadata, Document>,
class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource { UrlImportableSource {
private val prefs: PreferencesHelper by injectLazy()
override val id = HITOMI_SOURCE_ID
/**
* Whether the source has support for latest updates.
*/
override val supportsLatest = true
/**
* Name of the source.
*/
override val name = "hitomi.la"
/**
* The class of the metadata used by this source
*/
override val metaClass = HitomiSearchMetadata::class override val metaClass = HitomiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang
override val id: Long
get() = if (delegate.lang == "other") otherId else delegate.id
private var cachedTagIndexVersion: Long? = null // Support direct URL importing
private var tagIndexVersionCacheTime: Long = 0 override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
private fun tagIndexVersion(): Single<Long> { urlImportFetchSearchManga(context, query) {
val sCachedTagIndexVersion = cachedTagIndexVersion super.fetchSearchManga(page, query, filters)
return if (sCachedTagIndexVersion == null ||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedTagIndexVersion)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
} }
private var cachedGalleryIndexVersion: Long? = null
private var galleryIndexVersionCacheTime: Long = 0
private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null ||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it
galleryIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedGalleryIndexVersion)
}
}
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = input.location() url = input.location()
@@ -109,306 +58,63 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
title = galleryElement.selectFirst("h1").text() title = galleryElement.selectFirst("h1").text()
artists = galleryElement.select("h2 a").map { it.text() } artists = galleryElement.select("h2 a").map { it.text() }
tags += artists.map { RaisedTag("artist", it, TAG_TYPE_VIRTUAL) } tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
input.select(".gallery-info tr").forEach { input.select(".gallery-info tr").forEach {
val content = it.child(1) val content = it.child(1)
when (it.child(0).text().toLowerCase()) { when (it.child(0).text().toLowerCase()) {
"group" -> { "group" -> {
group = content.text() group = content.text()
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL) tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
"type" -> { "type" -> {
type = content.text() type = content.text()
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL) tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
"series" -> { "series" -> {
series = content.select("a").map { it.text() } series = content.select("a").map { it.text() }
tags += series.map { tags += series.map {
RaisedTag("series", it, TAG_TYPE_VIRTUAL) RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
} }
"language" -> { "language" -> {
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1) language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
language?.let { language?.let {
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL) tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
} }
"characters" -> { "characters" -> {
characters = content.select("a").map { it.text() } characters = content.select("a").map { it.text() }
tags += characters.map { RaisedTag("character", it, TAG_TYPE_DEFAULT) } tags += characters.map {
RaisedTag(
"character", it,
HitomiSearchMetadata.TAG_TYPE_DEFAULT
)
}
} }
"tags" -> { "tags" -> {
tags += content.select("a").map { tags += content.select("a").map {
val ns = if (it.attr("href").startsWith("/tag/male")) "male" val ns = if (it.attr("href").startsWith("/tag/male")) "male"
else if (it.attr("href").startsWith("/tag/female")) "female" else if (it.attr("href").startsWith("/tag/female")) "female"
else "misc" else "misc"
RaisedTag(ns, it.text().dropLast(if (ns == "misc") 0 else 2), TAG_TYPE_DEFAULT) RaisedTag(
ns, it.text().dropLast(if (ns == "misc") 0 else 2),
HitomiSearchMetadata.TAG_TYPE_DEFAULT
)
} }
} }
} }
} }
uploadDate = DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time uploadDate = try {
DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
} catch (e: Exception) {
null
}
} }
} }
override val lang = "all" override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
override val baseUrl = BASE_URL
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/popular-all.nozomi",
100L * (page - 1),
99L + 100 * (page - 1)
)
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return urlImportFetchSearchManga(context, query) {
val splitQuery = query.split(" ")
val positive = splitQuery.filter { !it.startsWith('-') }.toMutableList()
val negative = (splitQuery - positive).map { it.removePrefix("-") }
// TODO Cache the results coming out of HitomiNozomi
val hn = Single.zip(tagIndexVersion(), galleryIndexVersion()) { tv, gv -> tv to gv }
.map { HitomiNozomi(client, it.first, it.second) }
var base = if (positive.isEmpty()) {
hn.flatMap { n -> n.getGalleryIdsFromNozomi(null, "index", "all").map { n to it.toSet() } }
} else {
val q = positive.removeAt(0)
hn.flatMap { n -> n.getGalleryIdsForQuery(q).map { n to it.toSet() } }
}
base = positive.fold(base) { acc, q ->
acc.flatMap { (nozomi, mangas) ->
nozomi.getGalleryIdsForQuery(q).map {
nozomi to mangas.intersect(it)
}
}
}
base = negative.fold(base) { acc, q ->
acc.flatMap { (nozomi, mangas) ->
nozomi.getGalleryIdsForQuery(q).map {
nozomi to (mangas - it)
}
}
}
base.flatMap { (_, ids) ->
val chunks = ids.chunked(PAGE_SIZE)
nozomiIdsToMangas(chunks[page - 1]).map { mangas ->
MangasPage(mangas, page < chunks.size)
}
}.toObservable()
}
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/index-all.nozomi",
100L * (page - 1),
99L + 100 * (page - 1)
)
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.flatMap { responseToMangas(it) }
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.flatMap { responseToMangas(it) }
}
fun responseToMangas(response: Response): Observable<MangasPage> {
val range = response.header("Content-Range")!!
val total = range.substringAfter('/').toLong()
val end = range.substringBefore('/').substringAfter('-').toLong()
val body = response.body!!
return parseNozomiPage(body.bytes())
.map {
MangasPage(it, end < total - 1)
}
}
private fun parseNozomiPage(array: ByteArray): Observable<List<SManga>> {
val cursor = ByteCursor(array)
val ids = (1..array.size / 4).map {
cursor.nextInt()
}
return nozomiIdsToMangas(ids).toObservable()
}
private fun nozomiIdsToMangas(ids: List<Int>): Single<List<SManga>> {
return Single.zip(
ids.map {
client.newCall(GET("$LTN_BASE_URL/galleryblock/$it.html"))
.asObservableSuccess()
.subscribeOn(Schedulers.io()) // Perform all these requests in parallel
.map { parseGalleryBlock(it) }
.toSingle()
}
) { it.map { m -> m as SManga } }
}
private fun parseGalleryBlock(response: Response): SManga {
val doc = response.asJsoup()
return SManga.create().apply {
val titleElement = doc.selectFirst("h1")
title = titleElement.text()
thumbnail_url = "https:" + if (prefs.eh_hl_useHighQualityThumbs().get()) {
doc.selectFirst("img").attr("srcset").substringBefore(' ')
} else {
doc.selectFirst("img").attr("src")
}
url = titleElement.child(0).attr("href")
// TODO Parse tags and stuff
}
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
}
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 0.0f
}
)
)
}
override fun pageListRequest(chapter: SChapter): Request {
return GET("$LTN_BASE_URL/galleries/${HitomiSearchMetadata.hlIdFromUrl(chapter.url)}.js")
}
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
val hlId = response.request.url.pathSegments.last().removeSuffix(".js").toLong()
val str = response.body!!.string()
val json = JsonParser.parseString(str.removePrefix("var galleryinfo = "))
return json["files"].array.mapIndexed { index, jsonElement ->
val hash = jsonElement["hash"].string
val ext = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) jsonElement["name"].string.split('.').last() else "webp"
val path = if (jsonElement["haswebp"].string == "0" || !prefs.hitomiAlwaysWebp().get()) "images" else "webp"
val hashPath1 = hash.takeLast(1)
val hashPath2 = hash.takeLast(3).take(2)
Page(
index,
"",
"https://${subdomainFromGalleryId(hlId)}a.hitomi.la/$path/$hashPath1/$hashPath2/$hash.$ext"
)
}
}
private fun subdomainFromGalleryId(id: Long): Char {
return (97 + id.rem(NUMBER_OF_FRONTENDS)).toChar()
}
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val request = super.imageRequest(page)
val hlId = request.url.pathSegments.let {
it[it.lastIndex - 1]
}
return request.newBuilder()
.header("Referer", "$BASE_URL/reader/$hlId.html")
.build()
}
override val matchingHosts = listOf( override val matchingHosts = listOf(
"hitomi.la" "hitomi.la"
@@ -429,10 +135,7 @@ class Hitomi(val context: Context) : HttpSource(), LewdSource<HitomiSearchMetada
} }
companion object { companion object {
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10 const val otherId = 2703068117101782422L
private val PAGE_SIZE = 25
private val NUMBER_OF_FRONTENDS = 2
private val DATE_FORMAT by lazy { private val DATE_FORMAT by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US) SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
+19 -280
View File
@@ -8,115 +8,38 @@ import com.github.salomonbrys.kotson.nullLong
import com.github.salomonbrys.kotson.nullObj import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString import com.github.salomonbrys.kotson.nullString
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
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.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.NHENTAI_SOURCE_ID
import exh.metadata.metadata.NHentaiSearchMetadata import exh.metadata.metadata.NHentaiSearchMetadata
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
/** open class NHentai(delegate: HttpSource, val context: Context) :
* NHentai source DelegatedHttpSource(delegate),
*/ LewdSource<NHentaiSearchMetadata, Response>,
UrlImportableSource {
class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
override val metaClass = NHentaiSearchMetadata::class override val metaClass = NHentaiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override val id: Long
// TODO There is currently no way to get the most popular mangas get() = if (delegate.lang == "other") otherId else delegate.id
// TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
return fetchLatestUpdates(page)
}
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
val trimmedIdQuery = query.trim().removePrefix("id:") urlImportFetchSearchManga(context, query) {
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) { super.fetchSearchManga(page, query, filters)
"$baseUrl/g/$trimmedIdQuery/"
} else query
return urlImportFetchSearchManga(context, newQuery) {
searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess()
}.map { response ->
searchMangaParse(response)
}
}
}
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
val advQuery = combineQuery(filters)
val favoriteFilter = filters.findInstance<FavoriteFilter>()
val uploadedFilter = filters.findInstance<UploadedFilter>()
val url: HttpUrl.Builder
if (favoriteFilter != null && favoriteFilter.state) {
url = "$baseUrl/favorites".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("q", "$query $advQuery")
.addQueryParameter("page", page.toString())
} else {
url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("q", "$query $advQuery")
.addQueryParameter("page", page.toString())
if (uploadedFilter?.state?.isBlank() == true) {
filters.findInstance<SortFilter>()?.let { f ->
url.addQueryParameter("sort", f.toUriPart())
}
}
} }
return client.newCall(nhGet(url.toString()))
.asObservableSuccess()
.map { nhGet(url.toString(), page) }
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = parseResultPage(response)
override fun latestUpdatesRequest(page: Int): Request {
val uri = Uri.parse(baseUrl).buildUpon()
uri.appendQueryParameter("page", page.toString())
return nhGet(uri.toString(), page)
}
override fun latestUpdatesParse(response: Response) = parseResultPage(response)
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -131,37 +54,10 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
} }
} }
override fun mangaDetailsRequest(manga: SManga) = nhGet(baseUrl + manga.url)
private fun parseResultPage(response: Response): MangasPage {
val doc = response.asJsoup()
// TODO Parse lang + tags
val mangas = doc.select(".gallery > a").map {
SManga.create().apply {
url = it.attr("href")
title = it.selectFirst(".caption").text()
// last() is a hack to ignore the lazy-loader placeholder image on the front page
thumbnail_url = it.select("img").last().attr("src")
// In some pages, the thumbnail url does not include the protocol
if (!thumbnail_url!!.startsWith("https:")) thumbnail_url = "https:$thumbnail_url"
}
}
val hasNextPage = if (!response.request.url.queryParameterNames.contains(REVERSE_PARAM)) {
doc.selectFirst(".next") != null
} else {
response.request.url.queryParameter(REVERSE_PARAM)!!.toBoolean()
}
return MangasPage(mangas, hasNextPage)
}
override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) { override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(UNICODE_ESCAPE_REGEX) { it.groupValues[1].toInt(radix = 16).toChar().toString() } val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
UNICODE_ESCAPE_REGEX
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
val obj = JsonParser.parseString(json).asJsonObject val obj = JsonParser.parseString(json).asJsonObject
with(metadata) { with(metadata) {
@@ -198,164 +94,13 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
tags.clear() tags.clear()
}?.forEach { }?.forEach {
if (it.first != null && it.second != null) { if (it.first != null && it.second != null) {
tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") TAG_TYPE_VIRTUAL else TAG_TYPE_DEFAULT)) tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") RaisedSearchMetadata.TAG_TYPE_VIRTUAL else NHentaiSearchMetadata.TAG_TYPE_DEFAULT))
} }
} }
} }
} }
private fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) { override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
.asObservableSuccess()
.toSingle()
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 1f
}
)
)
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
if (metadata.mediaId == null) {
emptyList()
} else {
metadata.pageImageTypes.mapIndexed { index, s ->
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
Page(index, imageUrl!!, imageUrl)
}
}
}.toObservable()
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
private fun imageUrlFromType(mediaId: String, page: Int, t: String) = NHentaiSearchMetadata.typeToExtension(t)?.let {
"https://i.nhentai.net/galleries/$mediaId/$page.$it"
}
override fun chapterListParse(response: Response): List<SChapter> {
throw NotImplementedError("Unused method called!")
}
override fun pageListParse(response: Response): List<Page> {
throw NotImplementedError("Unused method called!")
}
override fun imageUrlParse(response: Response): String {
throw NotImplementedError("Unused method called!")
}
private fun combineQuery(filters: FilterList): String {
val stringBuilder = StringBuilder()
val advSearch = filters.filterIsInstance<AdvSearchEntryFilter>().flatMap { filter ->
val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank)
splitState.map {
AdvSearchEntry(filter.name, it.removePrefix("-"), it.startsWith("-"))
}
}
advSearch.forEach { entry ->
if (entry.exclude) stringBuilder.append("-")
stringBuilder.append("${entry.name}:")
stringBuilder.append(entry.text)
stringBuilder.append(" ")
}
val langFilter = filters.filterIsInstance<FilterLang>().firstOrNull()
if (langFilter != null) {
val language = SOURCE_LANG_LIST.first { it.first == langFilter.values[langFilter.state] }.second
if (!language.isBlank()) {
stringBuilder.append("language:$language")
}
}
return stringBuilder.toString()
}
data class AdvSearchEntry(val name: String, val text: String, val exclude: Boolean)
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TagFilter(),
CategoryFilter(),
GroupFilter(),
ArtistFilter(),
ParodyFilter(),
CharactersFilter(),
Filter.Header("Uploaded valid units are h, d, w, m, y."),
Filter.Header("example: (>20d)"),
UploadedFilter(),
Filter.Separator(),
SortFilter(),
Filter.Header("Sort is ignored if favorites only"),
FavoriteFilter(),
FilterLang()
)
class TagFilter : AdvSearchEntryFilter("Tags")
class CategoryFilter : AdvSearchEntryFilter("Categories")
class GroupFilter : AdvSearchEntryFilter("Groups")
class ArtistFilter : AdvSearchEntryFilter("Artists")
class ParodyFilter : AdvSearchEntryFilter("Parodies")
class CharactersFilter : AdvSearchEntryFilter("Characters")
class UploadedFilter : AdvSearchEntryFilter("Uploaded")
open class AdvSearchEntryFilter(name: String) : Filter.Text(name)
private class FavoriteFilter : Filter.CheckBox("Show favorites only", false)
// language filtering
private class FilterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
private class SortFilter : UriPartFilter(
"Sort By",
arrayOf(
Pair("Popular: All Time", "popular"),
Pair("Popular: Week", "popular-week"),
Pair("Popular: Today", "popular-today"),
Pair("Recent", "date")
)
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private val appName by lazy {
context.getString(R.string.app_name)
}
private fun nhGet(url: String, tag: Any? = null) = GET(url)
.newBuilder()
.header(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/56.0.2924.87 " +
"Safari/537.36 " +
"$appName/${BuildConfig.VERSION_CODE}"
)
.tag(tag).build()
override val id = NHENTAI_SOURCE_ID
override val lang = "all"
override val name = "nhentai"
override val baseUrl = NHentaiSearchMetadata.BASE_URL
override val supportsLatest = true
// === URL IMPORT STUFF
override val matchingHosts = listOf( override val matchingHosts = listOf(
"nhentai.net" "nhentai.net"
@@ -374,15 +119,9 @@ class NHentai(val context: Context) : HttpSource(), LewdSource<NHentaiSearchMeta
} }
companion object { companion object {
const val otherId = 7309872737163460316L
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);") private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})") private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
private const val REVERSE_PARAM = "TEH_REVERSE"
private val SOURCE_LANG_LIST = listOf(
Pair("All", ""),
Pair("English", "english"),
Pair("Japanese", "japanese"),
Pair("Chinese", "chinese")
)
} }
} }
+20 -252
View File
@@ -2,155 +2,47 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
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.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import exh.metadata.metadata.PervEdenLang
import exh.metadata.metadata.PervEdenSearchMetadata import exh.metadata.metadata.PervEdenSearchMetadata
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
import exh.util.UriFilter
import exh.util.UriGroup
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
// TODO Transform into delegated source class PervEden(delegate: HttpSource, val context: Context) :
class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Context) : DelegatedHttpSource(delegate),
ParsedHttpSource(),
LewdSource<PervEdenSearchMetadata, Document>, LewdSource<PervEdenSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
/**
* The class of the metadata used by this source
*/
override val metaClass = PervEdenSearchMetadata::class override val metaClass = PervEdenSearchMetadata::class
override val lang = delegate.lang
override val supportsLatest = true
override val name = "Perv Eden"
override val baseUrl = "http://www.perveden.com"
override val lang = pvLang.name
override fun popularMangaSelector() = "#topManga > ul > li"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = "http:" + element.select(".hottestImage > img").attr("data-src")
val titleElement = element.getElementsByClass("hottestInfo").first().child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text()
return manga
}
override fun popularMangaNextPageSelector(): String? = null
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaSelector() = "#mangaList > tbody > tr"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
val titleElement = element.child(0).child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text().trim()
return manga
}
override fun searchMangaNextPageSelector() = ".next"
override fun popularMangaRequest(page: Int): Request {
val urlLang = if (lang == "en") {
"eng"
} else {
"it"
}
return GET("$baseUrl/$urlLang/")
}
override fun latestUpdatesSelector() = ".newsManga"
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val header = element.getElementsByClass("manga_tooltop_header").first()
val titleElement = header.child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text().trim()
manga.thumbnail_url = "https:" + header.parent().selectFirst(".mangaImage img").attr("tmpsrc")
return manga
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
return MangasPage(mangas, mangas.isNotEmpty())
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon()
uri.appendQueryParameter("page", page.toString())
uri.appendQueryParameter("title", query)
filters.forEach {
if (it is UriFilter) it.addToUri(uri)
}
return GET(uri.toString())
}
override fun latestUpdatesNextPageSelector(): String? {
throw NotImplementedError("Unused method called!")
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen( parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
Observable.just(
manga.apply {
initialized = true
}
)
)
} }
} }
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = Uri.parse(input.location()).path url = Uri.parse(input.location()).path
@@ -184,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
"Artist" -> { "Artist" -> {
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
artist = it.text() artist = it.text()
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL) tags += RaisedTag(
"artist", it.text().toLowerCase(),
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
)
} }
} }
"Genres" -> { "Genres" -> {
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT) tags += RaisedTag(
null, it.text().toLowerCase(),
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
)
} }
} }
"Type" -> { "Type" -> {
@@ -218,137 +116,13 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
} }
} }
override fun mangaDetailsParse(document: Document): SManga = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request {
val num = when (lang) {
"en" -> "0"
"it" -> "1"
else -> throw NotImplementedError("Unimplemented language!")
}
return GET("$baseUrl/ajax/news/$page/$num/0/")
}
override fun chapterListSelector() = "#leftContent > table > tbody > tr"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
val linkElement = element.getElementsByClass("chapterLink").first()
setUrlWithoutDomain(linkElement.attr("href"))
name = "Chapter " + linkElement.getElementsByTag("b").text()
ChapterRecognition.parseChapterNumber(
this,
SManga.create().apply {
title = ""
}
)
try {
date_upload = DATE_FORMAT.parse(element.getElementsByClass("chapterDate").first().text().trim())!!.time
} catch (ignored: Exception) {
}
}
override fun pageListParse(document: Document) = document.getElementById("pageSelect").getElementsByTag("option").map {
Page(it.attr("data-page").toInt() - 1, baseUrl + it.attr("value"))
}
override fun imageUrlParse(document: Document) = "http:" + document.getElementById("mainImg").attr("src")!!
override fun getFilterList() = FilterList(
AuthorFilter(),
ArtistFilter(),
TypeFilterGroup(),
ReleaseYearGroup(),
StatusFilterGroup()
)
class StatusFilterGroup : UriGroup<StatusFilter>(
"Status",
listOf(
StatusFilter("Ongoing", 1),
StatusFilter("Completed", 2),
StatusFilter("Suspended", 0)
)
)
class StatusFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state) {
builder.appendQueryParameter("status", id.toString())
}
}
}
// Explicit type arg for listOf() to workaround this: KT-16570
class ReleaseYearGroup : UriGroup<Filter<*>>(
"Release Year",
listOf(
ReleaseYearRangeFilter(),
ReleaseYearYearFilter()
)
)
class ReleaseYearRangeFilter :
Filter.Select<String>(
"Range",
arrayOf(
"on",
"after",
"before"
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("releasedType", state.toString())
}
}
class ReleaseYearYearFilter : Filter.Text("Year"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("released", state)
}
}
class AuthorFilter : Filter.Text("Author"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("author", state)
}
}
class ArtistFilter : Filter.Text("Artist"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("artist", state)
}
}
class TypeFilterGroup : UriGroup<TypeFilter>(
"Type",
listOf(
TypeFilter("Japanese Manga", 0),
TypeFilter("Korean Manhwa", 1),
TypeFilter("Chinese Manhua", 2),
TypeFilter("Comic", 3),
TypeFilter("Doujinshi", 4)
)
)
class TypeFilter(n: String, val id: Int) : Filter.CheckBox(n, false), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state) {
builder.appendQueryParameter("type", id.toString())
}
}
}
override val matchingHosts = listOf("www.perveden.com") override val matchingHosts = listOf("www.perveden.com")
override fun matchesUri(uri: Uri): Boolean { override fun matchesUri(uri: Uri): Boolean {
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) { return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
PervEdenLang.en -> "en-manga" "en" -> "en-manga"
PervEdenLang.it -> "it-manga" "it" -> "it-manga"
else -> false
} }
} }
@@ -363,10 +137,4 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang, val context: Con
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter { override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
return PervEdenDescriptionAdapter(controller) return PervEdenDescriptionAdapter(controller)
} }
companion object {
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
timeZone = TimeZone.getTimeZone("GMT")
}
}
} }
@@ -2,252 +2,37 @@ package eu.kanade.tachiyomi.source.online.english
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kizitonwose.time.hours
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
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.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.EIGHTMUSES_SOURCE_ID
import exh.metadata.metadata.EightMusesSearchMetadata import exh.metadata.metadata.EightMusesSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
import exh.util.CachedField
import exh.util.NakedTrie
import exh.util.await
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.withContext
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers
typealias SiteMap = NakedTrie<Unit> class EightMuses(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
class EightMuses(val context: Context) :
HttpSource(),
LewdSource<EightMusesSearchMetadata, Document>, LewdSource<EightMusesSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val id = EIGHTMUSES_SOURCE_ID
/**
* Name of the source.
*/
override val name = "8muses"
/**
* Whether the source has support for latest updates.
*/
override val supportsLatest = true
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang: String = "en"
override val metaClass = EightMusesSearchMetadata::class override val metaClass = EightMusesSearchMetadata::class
override val lang = "en"
/** // Support direct URL importing
* Base url of the website without the trailing slash, like: http://mysite.com override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
*/ urlImportFetchSearchManga(context, query) {
override val baseUrl = EightMusesSearchMetadata.BASE_URL super.fetchSearchManga(page, query, filters)
private val siteMapCache = CachedField<SiteMap>(1.hours.inMilliseconds.longValue)
override val client: OkHttpClient
get() = network.cloudflareClient
private suspend fun obtainSiteMap() = siteMapCache.obtain {
withContext(Dispatchers.IO) {
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
.body!!.string()
val parsed = Jsoup.parse(result)
val seen = NakedTrie<Unit>()
parsed.getElementsByTag("loc").forEach { item ->
seen[item.text().substring(22)] = Unit
}
seen
}
}
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
.add("Referer", "https://www.8muses.com")
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
}
private fun eightMusesGet(url: String): Request {
return GET(url, headers = headersBuilder().build())
}
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int) = eightMusesGet("$baseUrl/comics/$page")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = if (!query.isBlank()) {
"$baseUrl/search".toHttpUrlOrNull()!!
.newBuilder()
.addQueryParameter("q", query)
} else {
"$baseUrl/comics".toHttpUrlOrNull()!!
.newBuilder()
} }
urlBuilder.addQueryParameter("page", page.toString())
filters.filterIsInstance<SortFilter>().map {
it.addToUri(urlBuilder)
}
return eightMusesGet(urlBuilder.toString())
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int) = eightMusesGet("$baseUrl/comics/lastupdate?page=$page")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
// override fun fetchLatestUpdates(page: Int) = fetchListing(latestUpdatesRequest(page), false)
override fun fetchLatestUpdates(page: Int) = fetchListing(popularMangaRequest(page), false)
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return urlImportFetchSearchManga(context, query) {
fetchListing(searchMangaRequest(page, query, filters), false)
}
}
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
return client.newCall(request)
.asObservableSuccess()
.flatMapSingle { response ->
RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
parseResultsPage(response, dig)
}.asSingle(GlobalScope.coroutineContext)
)
}
}
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
val doc = response.asJsoup()
val contents = parseSelf(doc)
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
return MangasPage(
if (dig) {
contents.albums.flatMap {
val href = it.attr("href")
val splitHref = href.split('/')
obtainSiteMap().subMap(href).filter {
it.key.split('/').size - splitHref.size == 1
}.map { (key, _) ->
SManga.create().apply {
url = key
title = key.substringAfterLast('/').replace('-', ' ')
}
}
}
} else {
contents.albums.map {
SManga.create().apply {
url = it.attr("href")
title = it.select(".title-text").text()
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
}
}
},
!onLastPage
)
}
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response): SManga {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -256,46 +41,6 @@ class EightMuses(val context: Context) :
} }
} }
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException("Should not be called!")
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
fetchAndParseChapterList("", manga.url)
}.asSingle(GlobalScope.coroutineContext)
).toObservable()
}
private suspend fun fetchAndParseChapterList(prefix: String, url: String): List<SChapter> {
// Request
val req = eightMusesGet(baseUrl + url)
return client.newCall(req).asObservableSuccess().toSingle().toBlocking().value().use { response ->
val contents = parseSelf(response.asJsoup())
val out = mutableListOf<SChapter>()
if (contents.images.isNotEmpty()) {
out += SChapter.create().apply {
this.url = url
this.name = if (prefix.isBlank()) ">" else prefix
}
}
val builtPrefix = if (prefix.isBlank()) "> " else "$prefix > "
out + contents.albums.flatMap { ele ->
fetchAndParseChapterList(builtPrefix + ele.selectFirst(".title-text").text(), ele.attr("href"))
}
}
}
data class SelfContents(val albums: List<Element>, val images: List<Element>) data class SelfContents(val albums: List<Element>, val images: List<Element>)
private fun parseSelf(doc: Document): SelfContents { private fun parseSelf(doc: Document): SelfContents {
@@ -309,22 +54,6 @@ class EightMuses(val context: Context) :
return SelfContents(selfAlbums, selfImages) return SelfContents(selfAlbums, selfImages)
} }
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
val contents = parseSelf(response.asJsoup())
return contents.images.mapIndexed { index, element ->
Page(
index,
element.attr("href"),
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
)
}
}
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
path = Uri.parse(input.location()).pathSegments path = Uri.parse(input.location()).pathSegments
@@ -355,40 +84,9 @@ class EightMuses(val context: Context) :
} }
} }
class SortFilter : Filter.Select<String>(
"Sort",
SORT_OPTIONS.map { it.second }.toTypedArray()
) {
fun addToUri(url: HttpUrl.Builder) {
url.addQueryParameter("sort", SORT_OPTIONS[state].first)
}
companion object {
// <Internal, Display>
private val SORT_OPTIONS = listOf(
"" to "Views",
"like" to "Likes",
"date" to "Date",
"az" to "A-Z"
)
}
}
override fun getFilterList() = FilterList(
SortFilter()
)
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException("Should not be called!")
}
override val matchingHosts = listOf( override val matchingHosts = listOf(
"www.8muses.com", "www.8muses.com",
"comics.8muses.com",
"8muses.com" "8muses.com"
) )
@@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.base.controller
interface ToolbarLiftOnScrollController
@@ -47,14 +47,15 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
version.text = extension.versionName version.text = extension.versionName
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context) lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
warning.text = when { warning.text = when {
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase() extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase() extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase() extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
// SY --> // SY -->
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase() extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
// SY <-- // SY <--
else -> null extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
} else -> ""
}.toUpperCase()
GlideApp.with(itemView.context).clear(image) GlideApp.with(itemView.context).clear(image)
if (extension is Extension.Available) { if (extension is Extension.Available) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> { private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
val (installed, untrusted, available) = tuple val (installed, untrusted, available) = tuple
val items = mutableListOf<ExtensionItem>() val items = mutableListOf<ExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName } val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName })) val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
val untrustedSorted = untrusted.sortedBy { it.pkgName } val untrustedSorted = untrusted.sortedBy { it.pkgName }
val availableSorted = available val availableSorted = available
// Filter out already installed extensions and disabled languages // Filter out already installed extensions and disabled languages
.filter { avail -> .filter { avail ->
installed.none { it.pkgName == avail.pkgName } && installed.none { it.pkgName == avail.pkgName } &&
untrusted.none { it.pkgName == avail.pkgName } && untrusted.none { it.pkgName == avail.pkgName } &&
(avail.lang in activeLangs || avail.lang == "all") (avail.lang in activeLangs || avail.lang == "all") &&
(showNsfwExtensions || !avail.isNsfw)
} }
.sortedBy { it.pkgName } .sortedBy { it.pkgName }
@@ -34,8 +34,8 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange import eu.kanade.tachiyomi.util.preference.onChange
@@ -50,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle), NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
NoToolbarElevationController { ToolbarLiftOnScrollController {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
binding.extensionTitle.text = extension.name binding.extensionTitle.text = extension.name
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName) binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context)) binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
binding.extensionNsfw.isVisible = extension.isNsfw
binding.extensionPkg.text = extension.pkgName binding.extensionPkg.text = extension.pkgName
binding.extensionUninstallButton.clicks() binding.extensionUninstallButton.clicks()
@@ -373,10 +373,10 @@ class MigrationListController(bundle: Bundle? = null) :
launchUI { launchUI {
val result = CoroutineScope(migratingManga.manga.migrationJob).async { val result = CoroutineScope(migratingManga.manga.migrationJob).async {
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id) val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
val chapters = source.fetchChapterList(localManga).toSingle().await(
Schedulers.io()
)
try { try {
val chapters = source.fetchChapterList(localManga).toSingle().await(
Schedulers.io()
)
syncChaptersWithSource(db, chapters, localManga, source) syncChaptersWithSource(db, chapters, localManga, source)
} catch (e: Exception) { } catch (e: Exception) {
return@async null return@async null
@@ -460,10 +460,6 @@ class MigrationListController(bundle: Bundle? = null) :
return true return true
} }
override fun onDestroyView(view: View) {
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.migration_list, menu) inflater.inflate(R.menu.migration_list, menu)
} }
@@ -207,7 +207,7 @@ class SourceController(bundle: Bundle? = null) :
) )
// SY <-- // SY <--
SourceOptionsDialog(item, items).showDialog(router) SourceOptionsDialog(item.source.toString(), items).showDialog(router)
} }
private fun disableSource(source: Source) { private fun disableSource(source: Source) {
@@ -396,17 +396,17 @@ class SourceController(bundle: Bundle? = null) :
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) { class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
private lateinit var item: SourceItem private lateinit var source: String
private lateinit var items: List<Pair<String, () -> Unit>> private lateinit var items: List<Pair<String, () -> Unit>>
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() { constructor(source: String, items: List<Pair<String, () -> Unit>>) : this() {
this.item = item this.source = source
this.items = items this.items = items
} }
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title(text = item.source.toString()) .title(text = source)
.listItems( .listItems(
items = items.map { it.first }, items = items.map { it.first },
waitForPositiveButton = false waitForPositiveButton = false
@@ -56,6 +56,7 @@ import eu.kanade.tachiyomi.widget.EmptyView
import exh.EXHSavedSearch import exh.EXHSavedSearch
import exh.isEhBasedSource import exh.isEhBasedSource
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.main_activity.root_coordinator
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@@ -578,7 +579,7 @@ open class BrowseSourceController(bundle: Bundle) :
binding.emptyView.show(message, actions) binding.emptyView.show(message, actions)
} else { } else {
snack = binding.catalogueView.snack(message, Snackbar.LENGTH_INDEFINITE) { snack = activity!!.root_coordinator?.snack(message, Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_retry, retryAction) setAction(R.string.action_retry, retryAction)
} }
} }
@@ -106,16 +106,10 @@ open class GlobalSearchPresenter(
val disabledSourceIds = preferences.disabledSources().get() val disabledSourceIds = preferences.disabledSources().get()
val pinnedSourceIds = preferences.pinnedSources().get() val pinnedSourceIds = preferences.pinnedSources().get()
val list = sourceManager.getVisibleCatalogueSources() return sourceManager.getVisibleCatalogueSources()
.filter { it.lang in languages } .filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds } .filterNot { it.id.toString() in disabledSourceIds }
.sortedBy { "(${it.lang}) ${it.name}" } .sortedWith(compareBy({ it.id.toString() !in pinnedSourceIds }, { "${it.name} (${it.lang})" }))
return if (preferences.searchPinnedSourcesOnly()) {
list.filter { it.id.toString() in pinnedSourceIds }
} else {
list.sortedBy { it.id.toString() !in pinnedSourceIds }
}
} }
private fun getSourcesToQuery(): List<CatalogueSource> { private fun getSourcesToQuery(): List<CatalogueSource> {
@@ -169,6 +163,8 @@ open class GlobalSearchPresenter(
val initialItems = sources.map { createCatalogueSearchItem(it, null) } val initialItems = sources.map { createCatalogueSearchItem(it, null) }
var items = initialItems var items = initialItems
val pinnedSourceIds = preferences.pinnedSources().get()
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchSourcesSubscription = Observable.from(sources) fetchSourcesSubscription = Observable.from(sources)
.flatMap( .flatMap(
@@ -186,7 +182,17 @@ open class GlobalSearchPresenter(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
// Update matching source with the obtained results // Update matching source with the obtained results
.map { result -> .map { result ->
items.map { item -> if (item.source == result.source) result else item } items
.map { item -> if (item.source == result.source) result else item }
.sortedWith(
compareBy(
// Bubble up sources that actually have results
{ it.results.isNullOrEmpty() },
// Same as initial sort, i.e. pinned first then alphabetically
{ it.source.id.toString() !in pinnedSourceIds },
{ "${it.source.name} (${it.source.lang})" }
)
)
} }
// Update current state // Update current state
.doOnNext { items = it } .doOnNext { items = it }
@@ -1,14 +1,23 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.ui.category.CategoryAdapter import eu.kanade.tachiyomi.ui.category.CategoryAdapter
import exh.isLewdSource import exh.isLewdSource
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.models.SearchTag
import exh.metadata.sql.models.SearchTitle
import exh.search.Namespace
import exh.search.QueryComponent
import exh.search.SearchEngine import exh.search.SearchEngine
import exh.search.Text
import exh.util.await import exh.util.await
import exh.util.cancellable import exh.util.cancellable
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -29,12 +38,18 @@ import uy.kohesive.injekt.injectLazy
* *
* @param view the fragment containing this adapter. * @param view the fragment containing this adapter.
*/ */
class LibraryCategoryAdapter(view: LibraryCategoryView) : class LibraryCategoryAdapter(view: LibraryCategoryView, val controller: LibraryController) :
FlexibleAdapter<LibraryItem>(null, view, true) { FlexibleAdapter<LibraryItem>(null, view, true) {
// EXH --> // EXH -->
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val searchEngine = SearchEngine() private val searchEngine = SearchEngine()
private var lastFilterJob: Job? = null private var lastFilterJob: Job? = null
private val sourceManager: SourceManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val hasLoggedServices by lazy {
trackManager.hasLoggedServices()
}
// Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter // Keep compatibility as searchText field was replaced when we upgraded FlexibleAdapter
var searchText var searchText
@@ -58,11 +73,11 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
* *
* @param list the list to set. * @param list the list to set.
*/ */
suspend fun setItems(cScope: CoroutineScope, list: List<LibraryItem>) { suspend fun setItems(scope: CoroutineScope, list: List<LibraryItem>) {
// A copy of manga always unfiltered. // A copy of manga always unfiltered.
mangas = list.toList() mangas = list.toList()
performFilter(cScope) performFilter(scope)
} }
/** /**
@@ -74,28 +89,30 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
return currentItems.indexOfFirst { it.manga.id == manga.id } return currentItems.indexOfFirst { it.manga.id == manga.id }
} }
fun canDrag() = (mode != Mode.MULTI || (mode == Mode.MULTI && selectedItemCount == 1)) &&
searchText.isBlank() &&
preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT &&
!preferences.downloadedOnly().get() &&
preferences.filterDownloaded().get() == Filter.TriState.STATE_IGNORE &&
preferences.filterCompleted().get() == Filter.TriState.STATE_IGNORE &&
preferences.filterUnread().get() == Filter.TriState.STATE_IGNORE &&
preferences.filterTracked().get() == Filter.TriState.STATE_IGNORE &&
preferences.filterLewd().get() == Filter.TriState.STATE_IGNORE
// EXH --> // EXH -->
// Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it // Note that we cannot use FlexibleAdapter's built in filtering system as we cannot cancel it
// (well technically we can cancel it by invoking filterItems again but that doesn't work when // (well technically we can cancel it by invoking filterItems again but that doesn't work when
// we want to perform a no-op filter) // we want to perform a no-op filter)
suspend fun performFilter(cScope: CoroutineScope) { suspend fun performFilter(scope: CoroutineScope) {
isLongPressDragEnabled = canDrag()
lastFilterJob?.cancel() lastFilterJob?.cancel()
if (mangas.isNotEmpty() && searchText.isNotBlank()) { if (mangas.isNotEmpty() && searchText.isNotBlank()) {
val savedSearchText = searchText val savedSearchText = searchText
val job = cScope.launch(Dispatchers.IO) { val job = scope.launch(Dispatchers.IO) {
val newManga = try { val newManga = try {
// Prepare filter object // Prepare filter object
val parsedQuery = searchEngine.parseQuery(savedSearchText) val parsedQuery = searchEngine.parseQuery(savedSearchText)
val sqlQuery = searchEngine.queryToSql(parsedQuery)
val queryResult = db.lowLevel().rawQuery(
RawQuery.builder()
.query(sqlQuery.first)
.args(*sqlQuery.second.toTypedArray())
.build()
)
ensureActive() // Fail early when cancelled
val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().await() val mangaWithMetaIdsQuery = db.getIdsOfFavoriteMangaWithMetadata().await()
val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count) val mangaWithMetaIds = LongArray(mangaWithMetaIdsQuery.count)
@@ -112,33 +129,20 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
ensureActive() // Fail early when cancelled ensureActive() // Fail early when cancelled
val convertedResult = LongArray(queryResult.count)
if (convertedResult.isNotEmpty()) {
val mangaIdCol = queryResult.getColumnIndex(SearchMetadataTable.COL_MANGA_ID)
queryResult.moveToFirst()
while (!queryResult.isAfterLast) {
ensureActive() // Fail early when cancelled
convertedResult[queryResult.position] = queryResult.getLong(mangaIdCol)
queryResult.moveToNext()
}
}
ensureActive() // Fail early when cancelled
// Flow the mangas to allow cancellation of this filter operation // Flow the mangas to allow cancellation of this filter operation
mangas.asFlow().cancellable().filter { item -> mangas.asFlow().cancellable().filter { item ->
if (isLewdSource(item.manga.source)) { if (isLewdSource(item.manga.source)) {
val mangaId = item.manga.id ?: -1 val mangaId = item.manga.id ?: -1
if (convertedResult.binarySearch(mangaId) < 0) { if (mangaWithMetaIds.binarySearch(mangaId) < 0) {
// Check if this manga even has metadata // No meta? Filter using title
if (mangaWithMetaIds.binarySearch(mangaId) < 0) { filterManga(parsedQuery, item.manga)
// No meta? Filter using title } else {
item.filter(savedSearchText) val tags = db.getSearchTagsForManga(mangaId).await()
} else false val titles = db.getSearchTitlesForManga(mangaId).await()
} else true filterManga(parsedQuery, item.manga, false, tags, titles)
}
} else { } else {
item.filter(savedSearchText) filterManga(parsedQuery, item.manga)
} }
}.toList() }.toList()
} catch (e: Exception) { } catch (e: Exception) {
@@ -159,5 +163,82 @@ class LibraryCategoryAdapter(view: LibraryCategoryView) :
updateDataSet(mangas) updateDataSet(mangas)
} }
} }
private suspend fun filterManga(queries: List<QueryComponent>, manga: LibraryManga, checkGenre: Boolean = true, searchTags: List<SearchTag>? = null, searchTitles: List<SearchTitle>? = null): Boolean {
val mappedQueries = queries.groupBy { it.excluded }
val tracks = if (hasLoggedServices) db.getTracks(manga).await().toList() else null
val source = sourceManager.get(manga.source)
val genre = if (checkGenre) manga.getGenres() else null
val hasNormalQuery = mappedQueries[false]?.all { queryComponent ->
when (queryComponent) {
is Text -> {
val query = queryComponent.asQuery()
manga.title.contains(query, true) ||
(manga.author?.contains(query, true) == true) ||
(manga.artist?.contains(query, true) == true) ||
(source?.name?.contains(query, true) == true) ||
(hasLoggedServices && tracks != null && filterTracks(query, tracks)) ||
(genre != null && genre.any { it.contains(query, true) }) ||
(searchTags != null && searchTags.any { it.name.contains(query, true) }) ||
(searchTitles != null && searchTitles.any { it.title.contains(query, true) })
}
is Namespace -> {
searchTags != null && searchTags.any {
val tag = queryComponent.tag
(it.namespace != null && it.namespace.contains(queryComponent.namespace, true) && tag != null && it.name.contains(tag.asQuery(), true)) ||
(tag == null && it.namespace != null && it.namespace.contains(queryComponent.namespace, true))
}
}
else -> true
}
}
val doesNotHaveExcludedQuery = mappedQueries[true]?.all { queryComponent ->
when (queryComponent) {
is Text -> {
val query = queryComponent.asQuery()
query.isBlank() || (
(!manga.title.contains(query, true)) &&
(manga.author == null || (manga.author?.contains(query, true) == false)) &&
(manga.artist == null || (manga.artist?.contains(query, true) == false)) &&
(source == null || !source.name.contains(query, true)) &&
(hasLoggedServices && tracks != null && !filterTracks(query, tracks)) &&
(genre == null || genre.all { !it.contains(query, true) }) &&
(searchTags == null || searchTags.all { !it.name.contains(query, true) }) ||
(searchTitles == null || searchTitles.all { !it.title.contains(query, true) })
)
}
is Namespace -> {
Timber.d(manga.title)
val tag = queryComponent.tag?.asQuery()
searchTags == null || searchTags.all {
if (tag == null || tag.isBlank()) {
it.namespace == null || !it.namespace.contains(queryComponent.namespace, true)
} else if (it.namespace == null) {
true
} else {
!(it.name.contains(tag, true) && it.namespace.contains(queryComponent.namespace, true))
}
}
}
else -> true
}
}
return (hasNormalQuery != null && doesNotHaveExcludedQuery != null && hasNormalQuery && doesNotHaveExcludedQuery) ||
(hasNormalQuery != null && doesNotHaveExcludedQuery == null && hasNormalQuery) ||
(hasNormalQuery == null && doesNotHaveExcludedQuery != null && doesNotHaveExcludedQuery)
}
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
return tracks.any {
val trackService = trackManager.getService(it.sync_id)
if (trackService != null) {
val status = trackService.getStatus(it.status)
val name = trackService.name
return@any status.contains(constraint, true) || name.contains(constraint, true)
}
return@any false
}
}
// EXH <-- // EXH <--
} }
@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.ui.LoadingHandle import exh.ui.LoadingHandle
import exh.util.removeArticles
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.android.synthetic.main.library_category.view.fast_scroller import kotlinx.android.synthetic.main.library_category.view.fast_scroller
import kotlinx.android.synthetic.main.library_category.view.swipe_refresh import kotlinx.android.synthetic.main.library_category.view.swipe_refresh
@@ -38,6 +37,7 @@ import reactivecircus.flowbinding.recyclerview.scrollStateChanges
import reactivecircus.flowbinding.swiperefreshlayout.refreshes import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@@ -56,6 +56,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val db: DatabaseHelper by injectLazy()
/** /**
* The fragment containing this view. * The fragment containing this view.
*/ */
@@ -86,7 +88,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// EXH --> // EXH -->
private var initialLoadHandle: LoadingHandle? = null private var initialLoadHandle: LoadingHandle? = null
lateinit var scope2: CoroutineScope private lateinit var supervisorScope: CoroutineScope
private fun newScope() = object : CoroutineScope { private fun newScope() = object : CoroutineScope {
override val coroutineContext = SupervisorJob() + Dispatchers.Main override val coroutineContext = SupervisorJob() + Dispatchers.Main
@@ -106,7 +108,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
} }
adapter = LibraryCategoryAdapter(this) adapter = LibraryCategoryAdapter(this, controller)
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
@@ -126,7 +128,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt()) swipe_refresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
swipe_refresh.refreshes() swipe_refresh.refreshes()
.onEach { .onEach {
if (LibraryUpdateService.start(context, category)) { if (LibraryUpdateService.start(context, if (preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT) category else null)) {
context.toast(R.string.updating_category) context.toast(R.string.updating_category)
} }
@@ -145,12 +147,11 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
SelectableAdapter.Mode.SINGLE SelectableAdapter.Mode.SINGLE
} }
// SY --> // SY -->
val sortingMode = preferences.librarySortingMode().get() adapter.isLongPressDragEnabled = adapter.canDrag()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP
// SY <-- // SY <--
// EXH --> // EXH -->
scope2 = newScope() supervisorScope = newScope()
initialLoadHandle = controller.loaderManager.openProgressBar() initialLoadHandle = controller.loaderManager.openProgressBar()
// EXH <-- // EXH <--
@@ -161,7 +162,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe {
// EXH --> // EXH -->
scope2.launch { supervisorScope.launch {
val handle = controller.loaderManager.openProgressBar() val handle = controller.loaderManager.openProgressBar()
try { try {
// EXH <-- // EXH <--
@@ -177,7 +178,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
subscriptions += controller.libraryMangaRelay subscriptions += controller.libraryMangaRelay
.subscribe { .subscribe {
// EXH --> // EXH -->
scope2.launch { supervisorScope.launch {
try { try {
// EXH <-- // EXH <--
onNextLibraryManga(this, it) onNextLibraryManga(this, it)
@@ -209,33 +210,6 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
} }
controller.invalidateActionMode() controller.invalidateActionMode()
} }
// SY -->
subscriptions += controller.reorganizeRelay
.subscribe {
if (it.first == category.id) {
var items = when (it.second) {
1, 2 -> adapter.currentItems.sortedBy {
// if (preferences.removeArticles().getOrDefault())
it.manga.title.removeArticles()
// else
// it.manga.title
}
3, 4 -> adapter.currentItems.sortedBy { it.manga.last_update }
else -> {
adapter.currentItems.sortedBy { it.manga.title }
}
}
if (it.second % 2 == 0) {
items = items.reversed()
}
runBlocking { adapter.setItems(this, items) }
adapter.notifyDataSetChanged()
}
controller.invalidateActionMode()
}
// }
// SY <--
} }
fun onRecycle() { fun onRecycle() {
@@ -249,7 +223,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
fun unsubscribe() { fun unsubscribe() {
subscriptions.clear() subscriptions.clear()
// EXH --> // EXH -->
scope2.cancel() supervisorScope.cancel()
controller.loaderManager.closeProgressBar(initialLoadHandle) controller.loaderManager.closeProgressBar(initialLoadHandle)
// EXH <-- // EXH <--
} }
@@ -264,18 +238,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
// Get the manga list for this category. // Get the manga list for this category.
// SY --> // SY -->
val sortingMode = preferences.librarySortingMode().get() val sortingMode = preferences.librarySortingMode().get()
adapter.isLongPressDragEnabled = sortingMode == LibrarySort.DRAG_AND_DROP adapter.isLongPressDragEnabled = adapter.canDrag()
var mangaForCategory = event.getMangaForCategory(category).orEmpty() var mangaForCategory = event.getMangaForCategory(category).orEmpty()
if (sortingMode == LibrarySort.DRAG_AND_DROP) { if (sortingMode == LibrarySort.DRAG_AND_DROP) {
if (category.name == "Default") { if (category.id == 0) {
category.mangaOrder = preferences.defaultMangaOrder().get().split("/") category.mangaOrder = preferences.defaultMangaOrder().get()
.split("/")
.mapNotNull { it.toLongOrNull() } .mapNotNull { it.toLongOrNull() }
} }
mangaForCategory = mangaForCategory.sortedBy { mangaForCategory = mangaForCategory.sortedBy {
category.mangaOrder.indexOf( category.mangaOrder.indexOf(it.manga.id)
it.manga
.id
)
} }
} }
// SY <-- // SY <--
@@ -307,7 +279,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
if (adapter.mode != SelectableAdapter.Mode.MULTI) { if (adapter.mode != SelectableAdapter.Mode.MULTI) {
adapter.mode = SelectableAdapter.Mode.MULTI adapter.mode = SelectableAdapter.Mode.MULTI
// SY --> // SY -->
adapter.isLongPressDragEnabled = false adapter.isLongPressDragEnabled = adapter.canDrag()
// SY <-- // SY <--
} }
findAndToggleSelection(event.manga) findAndToggleSelection(event.manga)
@@ -318,8 +290,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
if (controller.selectedMangas.isEmpty()) { if (controller.selectedMangas.isEmpty()) {
adapter.mode = SelectableAdapter.Mode.SINGLE adapter.mode = SelectableAdapter.Mode.SINGLE
// SY --> // SY -->
adapter.isLongPressDragEnabled = preferences.librarySortingMode() adapter.isLongPressDragEnabled = adapter.canDrag()
.get() == LibrarySort.DRAG_AND_DROP
// SY <-- // SY <--
} }
} }
@@ -328,8 +299,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
adapter.clearSelection() adapter.clearSelection()
lastClickPosition = -1 lastClickPosition = -1
// SY --> // SY -->
adapter.isLongPressDragEnabled = preferences.librarySortingMode() adapter.isLongPressDragEnabled = adapter.canDrag()
.get() == LibrarySort.DRAG_AND_DROP
// SY <-- // SY <--
} }
} }
@@ -375,7 +345,7 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
controller.createActionModeIfNeeded() controller.createActionModeIfNeeded()
// SY --> // SY -->
adapter.isLongPressDragEnabled = false adapter.isLongPressDragEnabled = adapter.canDrag()
// SY <-- // SY <--
when { when {
lastClickPosition == -1 -> setSelection(position) lastClickPosition == -1 -> setSelection(position)
@@ -390,29 +360,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
lastClickPosition = position lastClickPosition = position
} }
// SY --> // SY -->
override fun onItemMove(fromPosition: Int, toPosition: Int) {
}
override fun onItemReleased(position: Int) { override fun onItemReleased(position: Int) {
if (adapter.selectedItemCount == 0) { return
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
category.mangaOrder = mangaIds
val db: DatabaseHelper by injectLazy()
if (category.name == "Default") {
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
} else {
db.insertCategory(category).asRxObservable().subscribe()
}
}
} }
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean { override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
if (adapter.selectedItemCount > 1) { if (adapter.isSelected(fromPosition)) toggleSelection(fromPosition)
return false
}
if (adapter.isSelected(fromPosition)) {
toggleSelection(fromPosition)
}
return true return true
} }
@@ -422,6 +375,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
onItemLongClick(position) onItemLongClick(position)
} }
} }
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) return
controller.invalidateActionMode()
val mangaIds = adapter.currentItems.mapNotNull { it.manga.id }
category.mangaOrder = mangaIds
if (category.id == 0) {
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
} else {
db.insertCategory(category).asRxObservable().subscribe()
}
if (preferences.librarySortingMode().get() != LibrarySort.DRAG_AND_DROP) {
preferences.librarySortingAscending().set(true)
preferences.librarySortingMode().set(LibrarySort.DRAG_AND_DROP)
controller.refreshSort()
}
}
// SY <-- // SY <--
/** /**
@@ -13,9 +13,13 @@ import kotlinx.android.synthetic.main.source_comfortable_grid_item.badges
import kotlinx.android.synthetic.main.source_comfortable_grid_item.card import kotlinx.android.synthetic.main.source_comfortable_grid_item.card
import kotlinx.android.synthetic.main.source_comfortable_grid_item.download_text import kotlinx.android.synthetic.main.source_comfortable_grid_item.download_text
import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text
import kotlinx.android.synthetic.main.source_comfortable_grid_item.play_layout
import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
import kotlinx.android.synthetic.main.source_comfortable_grid_item.unread_text import kotlinx.android.synthetic.main.source_comfortable_grid_item.unread_text
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -31,6 +35,16 @@ class LibraryComfortableGridHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>> adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
) : LibraryCompactGridHolder(view, adapter) { ) : LibraryCompactGridHolder(view, adapter) {
// SY -->
init {
play_layout.clicks()
.onEach {
playButtonClicked()
}
.launchIn((adapter as LibraryCategoryAdapter).controller.scope)
}
// SY <--
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga. * holder with the given manga.
@@ -38,6 +52,9 @@ class LibraryComfortableGridHolder(
* @param item the manga item to bind. * @param item the manga item to bind.
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// SY -->
manga = item.manga
// SY <--
// Update the title of the manga. // Update the title of the manga.
title.text = item.manga.title title.text = item.manga.title
@@ -57,6 +74,10 @@ class LibraryComfortableGridHolder(
// set local visibility if its local manga // set local visibility if its local manga
local_text.isVisible = item.manga.isLocal() local_text.isVisible = item.manga.isLocal()
// SY -->
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
// SY <--
// For rounded corners // For rounded corners
card.clipToOutline = true card.clipToOutline = true
@@ -6,6 +6,7 @@ import androidx.recyclerview.widget.RecyclerView
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.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
@@ -13,9 +14,13 @@ import kotlinx.android.synthetic.main.source_compact_grid_item.badges
import kotlinx.android.synthetic.main.source_compact_grid_item.card import kotlinx.android.synthetic.main.source_compact_grid_item.card
import kotlinx.android.synthetic.main.source_compact_grid_item.download_text import kotlinx.android.synthetic.main.source_compact_grid_item.download_text
import kotlinx.android.synthetic.main.source_compact_grid_item.local_text import kotlinx.android.synthetic.main.source_compact_grid_item.local_text
import kotlinx.android.synthetic.main.source_compact_grid_item.play_layout
import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_compact_grid_item.title import kotlinx.android.synthetic.main.source_compact_grid_item.title
import kotlinx.android.synthetic.main.source_compact_grid_item.unread_text import kotlinx.android.synthetic.main.source_compact_grid_item.unread_text
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
/** /**
* Class used to hold the displayed data of a manga in the library, like the cover or the title. * Class used to hold the displayed data of a manga in the library, like the cover or the title.
@@ -33,6 +38,18 @@ open class LibraryCompactGridHolder(
// SY <-- // SY <--
) : LibraryHolder(view, adapter) { ) : LibraryHolder(view, adapter) {
var manga: Manga? = null
// SY -->
init {
play_layout.clicks()
.onEach {
playButtonClicked()
}
.launchIn((adapter as LibraryCategoryAdapter).controller.scope)
}
// SY <--
/** /**
* Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this * Method called from [LibraryCategoryAdapter.onBindViewHolder]. It updates the data for this
* holder with the given manga. * holder with the given manga.
@@ -40,6 +57,9 @@ open class LibraryCompactGridHolder(
* @param item the manga item to bind. * @param item the manga item to bind.
*/ */
override fun onSetValues(item: LibraryItem) { override fun onSetValues(item: LibraryItem) {
// SY -->
manga = item.manga
// SY <--
// Update the title of the manga. // Update the title of the manga.
title.text = item.manga.title title.text = item.manga.title
@@ -59,6 +79,10 @@ open class LibraryCompactGridHolder(
// set local visibility if its local manga // set local visibility if its local manga
local_text.isVisible = item.manga.isLocal() local_text.isVisible = item.manga.isLocal()
// SY -->
play_layout.isVisible = (item.manga.unread > 0 && item.startReadingButton)
// SY <--
// For rounded corners // For rounded corners
card.clipToOutline = true card.clipToOutline = true
@@ -71,4 +95,10 @@ open class LibraryCompactGridHolder(
.dontAnimate() .dontAnimate()
.into(thumbnail) .into(thumbnail)
} }
// SY -->
fun playButtonClicked() {
manga?.let { (adapter as LibraryCategoryAdapter).controller.startReading(it, (adapter as LibraryCategoryAdapter)) }
}
// SY <--
} }
@@ -21,6 +21,7 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import eu.davidea.flexibleadapter.SelectableAdapter
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
@@ -39,10 +40,16 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight import eu.kanade.tachiyomi.ui.main.offsetAppbarHeight
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus import exh.favorites.FavoritesSyncStatus
import exh.nHentaiSourceIds
import exh.ui.LoaderManager import exh.ui.LoaderManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.android.synthetic.main.main_activity.tabs
@@ -113,13 +120,6 @@ class LibraryController(
*/ */
val selectInverseRelay: PublishRelay<Int> = PublishRelay.create() val selectInverseRelay: PublishRelay<Int> = PublishRelay.create()
// SY -->
/**
* Relay to notify the library's viewpager to reotagnize all
*/
val reorganizeRelay: PublishRelay<Pair<Int, Int>> = PublishRelay.create()
// SY <--
/** /**
* Number of manga per row in grid mode. * Number of manga per row in grid mode.
*/ */
@@ -215,7 +215,13 @@ class LibraryController(
is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged() is LibrarySettingsSheet.Sort.SortGroup -> onSortChanged()
is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter() is LibrarySettingsSheet.Display.DisplayGroup -> reattachAdapter()
is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged() is LibrarySettingsSheet.Display.BadgeGroup -> onBadgeSettingChanged()
// SY -->
is LibrarySettingsSheet.Display.ButtonsGroup -> onButtonSettingChanged()
// SY <--
is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged() is LibrarySettingsSheet.Display.TabsGroup -> onTabsSettingsChanged()
// SY -->
is LibrarySettingsSheet.Grouping.InternalGroup -> onGroupSettingChanged()
// SY <--
} }
} }
@@ -338,6 +344,16 @@ class LibraryController(
presenter.requestBadgesUpdate() presenter.requestBadgesUpdate()
} }
// SY -->
private fun onButtonSettingChanged() {
presenter.requestButtonsUpdate()
}
private fun onGroupSettingChanged() {
presenter.requestGroupsUpdate()
}
// SY <--
private fun onTabsSettingsChanged() { private fun onTabsSettingsChanged() {
tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1) tabsVisibilityRelay.call(preferences.categoryTabs().get() && adapter?.categories?.size ?: 0 > 1)
updateTitle() updateTitle()
@@ -391,11 +407,6 @@ class LibraryController(
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu) inflater.inflate(R.menu.library, menu)
// SY -->
val reorganizeItem = menu.findItem(R.id.action_reorganize)
reorganizeItem.isVisible = preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP
// SY <--
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE searchView.maxWidth = Int.MAX_VALUE
@@ -483,24 +494,12 @@ class LibraryController(
presenter.favoritesSync.runSync() presenter.favoritesSync.runSync()
} }
} }
R.id.action_alpha_asc -> reOrder(1)
R.id.action_alpha_dsc -> reOrder(2)
R.id.action_update_asc -> reOrder(3)
R.id.action_update_dsc -> reOrder(4)
// SY <-- // SY <--
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
// SY -->
private fun reOrder(type: Int) {
adapter?.categories?.getOrNull(binding.libraryPager.currentItem)?.id?.let {
reorganizeRelay.call(it to type)
}
}
// SY <--
/** /**
* Invalidates the action mode, forcing it to refresh its content. * Invalidates the action mode, forcing it to refresh its content.
*/ */
@@ -522,6 +521,16 @@ class LibraryController(
mode.title = count.toString() mode.title = count.toString()
binding.actionToolbar.findItem(R.id.action_download_unread)?.isVisible = selectedMangas.any { it.source != LocalSource.ID } binding.actionToolbar.findItem(R.id.action_download_unread)?.isVisible = selectedMangas.any { it.source != LocalSource.ID }
// SY -->
binding.actionToolbar.findItem(R.id.action_clean)?.isVisible = selectedMangas.any {
it.source == EH_SOURCE_ID ||
it.source == EXH_SOURCE_ID ||
it.source in nHentaiSourceIds ||
it.source == PERV_EDEN_EN_SOURCE_ID ||
it.source == PERV_EDEN_IT_SOURCE_ID
}
// SY <--
} }
return false return false
} }
@@ -534,15 +543,19 @@ class LibraryController(
when (item.itemId) { when (item.itemId) {
R.id.action_move_to_category -> showChangeMangaCategoriesDialog() R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
R.id.action_download_unread -> downloadUnreadChapters() R.id.action_download_unread -> downloadUnreadChapters()
R.id.action_mark_as_read -> markReadStatus(true)
R.id.action_mark_as_unread -> markReadStatus(false)
R.id.action_delete -> showDeleteMangaDialog() R.id.action_delete -> showDeleteMangaDialog()
R.id.action_select_all -> selectAllCategoryManga() R.id.action_select_all -> selectAllCategoryManga()
R.id.action_select_inverse -> selectInverseCategoryManga() R.id.action_select_inverse -> selectInverseCategoryManga()
// SY --> // SY -->
R.id.action_migrate -> { R.id.action_migrate -> {
val skipPre = preferences.skipPreMigration().get() val skipPre = preferences.skipPreMigration().get()
PreMigrationController.navigateToMigration(skipPre, router, selectedMangas.mapNotNull { it.id }) val selectedMangaIds = selectedMangas.mapNotNull { it.id }
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
} }
R.id.action_clean -> cleanTitles()
// SY <-- // SY <--
else -> return false else -> return false
} }
@@ -623,10 +636,30 @@ class LibraryController(
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
private fun markReadStatus(read: Boolean) {
val mangas = selectedMangas.toList()
presenter.markReadStatus(mangas, read)
destroyActionModeIfNeeded()
}
private fun showDeleteMangaDialog() { private fun showDeleteMangaDialog() {
DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router) DeleteLibraryMangasDialog(this, selectedMangas.toList()).showDialog(router)
} }
// SY -->
private fun cleanTitles() {
val mangas = selectedMangas.filter {
it.source == EH_SOURCE_ID ||
it.source == EXH_SOURCE_ID ||
it.source in nHentaiSourceIds ||
it.source == PERV_EDEN_EN_SOURCE_ID ||
it.source == PERV_EDEN_IT_SOURCE_ID
}.toList()
presenter.cleanTitles(mangas)
destroyActionModeIfNeeded()
}
// SY <--
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
presenter.moveMangasToCategories(categories, mangas) presenter.moveMangasToCategories(categories, mangas)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
@@ -775,5 +808,21 @@ class LibraryController(
} }
oldSyncStatus = status oldSyncStatus = status
} }
fun startReading(manga: Manga, adapter: LibraryCategoryAdapter) {
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
toggleSelection(manga)
return
}
val activity = activity ?: return
val chapter = presenter.getFirstUnread(manga) ?: return
val intent = ReaderActivity.newIntent(activity, manga, chapter)
destroyActionModeIfNeeded()
startActivity(intent)
}
fun refreshSort() {
settingsSheet?.refreshSort()
}
// <-- EXH // <-- EXH
} }
@@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.library
import eu.kanade.tachiyomi.R
object LibraryGroup {
const val BY_DEFAULT = 0
const val BY_SOURCE = 1
const val BY_STATUS = 2
const val BY_TRACK_STATUS = 3
const val UNGROUPED = 4
fun groupTypeStringRes(type: Int, hasCategories: Boolean = true): Int {
return when (type) {
BY_STATUS -> R.string.status
BY_SOURCE -> R.string.label_sources
BY_TRACK_STATUS -> R.string.tracking_status
UNGROUPED -> R.string.ungrouped
else -> if (hasCategories) R.string.categories else R.string.ungrouped
}
}
fun groupTypeDrawableRes(type: Int): Int {
return when (type) {
BY_STATUS -> R.drawable.ic_progress_clock_24dp
BY_TRACK_STATUS -> R.drawable.ic_sync_24dp
BY_SOURCE -> R.drawable.ic_explore_24dp
UNGROUPED -> R.drawable.ic_ungroup_24dp
else -> R.drawable.ic_label_24dp
}
}
}
@@ -38,5 +38,12 @@ abstract class LibraryHolder(
super.onItemReleased(position) super.onItemReleased(position)
(adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position) (adapter as? LibraryCategoryAdapter)?.onItemReleaseListener?.onItemReleased(position)
} }
override fun onLongClick(view: View?): Boolean {
return if (adapter.isLongPressDragEnabled) {
super.onLongClick(view)
false
} else super.onLongClick(view)
}
// SY <-- // SY <--
} }
@@ -19,23 +19,35 @@ import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.isNamespaceSource
import exh.metadata.metadata.base.RaisedTag
import exh.util.SourceTagsUtil.Companion.TAG_TYPE_EXCLUDE
import exh.util.SourceTagsUtil.Companion.getRaisedTags
import exh.util.SourceTagsUtil.Companion.parseTag
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) : class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Preference<DisplayMode>) :
AbstractFlexibleItem<LibraryHolder>(), IFilterable<String> { AbstractFlexibleItem<LibraryHolder>(), IFilterable<Pair<String, Boolean>> {
private val sourceManager: SourceManager = Injekt.get() private val sourceManager: SourceManager = Injekt.get()
// SY --> // SY -->
private val trackManager: TrackManager = Injekt.get() private val trackManager: TrackManager = Injekt.get()
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get()
private val source by lazy {
sourceManager.get(manga.source)
}
// SY <-- // SY <--
var downloadCount = -1 var downloadCount = -1
var unreadCount = -1 var unreadCount = -1
// SY -->
var startReadingButton = false
// SY <--
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return when (libraryDisplayMode.get()) { return when (libraryDisplayMode.get()) {
DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item
@@ -96,38 +108,13 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
* @param constraint the query to apply. * @param constraint the query to apply.
* @return true if the manga should be included, false otherwise. * @return true if the manga should be included, false otherwise.
*/ */
override fun filter(constraint: String): Boolean { override fun filter(constraint: Pair<String, Boolean>): Boolean {
return manga.title.contains(constraint, true) || return manga.title.contains(constraint.first, true) ||
(manga.author?.contains(constraint, true) ?: false) || (manga.author?.contains(constraint.first, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) || (manga.artist?.contains(constraint.first, true) ?: false) ||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) || (source?.name?.contains(constraint.first, true) ?: false) ||
(Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint, db.getTracks(manga).executeAsBlocking())) || (Injekt.get<TrackManager>().hasLoggedServices() && filterTracks(constraint.first, db.getTracks(manga).executeAsBlocking())) ||
if (constraint.contains(" ") || constraint.contains("\"")) { constraint.second && ehContainsGenre(constraint.first)
val genres = manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
}
var clean_constraint = ""
var ignorespace = false
for (i in constraint.trim().toLowerCase()) {
if (i == ' ') {
if (!ignorespace) {
clean_constraint = clean_constraint + ","
} else {
clean_constraint = clean_constraint + " "
}
} else if (i == '"') {
ignorespace = !ignorespace
} else {
clean_constraint = clean_constraint + Character.toString(i)
}
}
clean_constraint.split(",").all { containsGenre(it.trim(), genres) }
} else containsGenre(
constraint,
manga.genre?.split(", ")?.map {
it.drop(it.indexOfFirst { it == ':' } + 1).toLowerCase().trim() // tachiEH tag namespaces
}
)
} }
private fun filterTracks(constraint: String, tracks: List<Track>): Boolean { private fun filterTracks(constraint: String, tracks: List<Track>): Boolean {
@@ -141,6 +128,54 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
return@any false return@any false
} }
} }
private fun ehContainsGenre(constraint: String): Boolean {
val genres = manga.getGenres()
val raisedTags = if (source?.isNamespaceSource() == true) {
manga.getRaisedTags(genres)
} else null
return if (constraint.contains(" ") || constraint.contains("\"")) {
var cleanConstraint = ""
var ignoreSpace = false
for (i in constraint.trim().toLowerCase()) {
when (i) {
' ' -> {
cleanConstraint = if (!ignoreSpace) {
"$cleanConstraint,"
} else {
"$cleanConstraint "
}
}
'"' -> {
ignoreSpace = !ignoreSpace
}
else -> {
cleanConstraint += i.toString()
}
}
}
cleanConstraint.split(",").all {
if (raisedTags == null) containsGenre(it.trim(), genres) else containsRaisedGenre(
parseTag(it.trim()), raisedTags
)
}
} else if (raisedTags == null) {
containsGenre(constraint, genres)
} else {
containsRaisedGenre(parseTag(constraint), raisedTags)
}
}
private fun containsRaisedGenre(tag: RaisedTag, genres: List<RaisedTag>): Boolean {
val genre = genres.find {
(it.namespace?.toLowerCase() == tag.namespace?.toLowerCase() && it.name.toLowerCase() == tag.name.toLowerCase())
}
return if (tag.type == TAG_TYPE_EXCLUDE) {
genre == null
} else {
genre != null
}
}
// SY <-- // SY <--
private fun containsGenre(tag: String, genres: List<String>?): Boolean { private fun containsGenre(tag: String, genres: List<String>?): Boolean {
@@ -2,13 +2,17 @@ package eu.kanade.tachiyomi.ui.library
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.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.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_EXCLUDE
import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE import eu.kanade.tachiyomi.source.model.Filter.TriState.Companion.STATE_IGNORE
@@ -25,6 +29,7 @@ import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.favorites.FavoritesSyncHelper import exh.favorites.FavoritesSyncHelper
import exh.util.isLewd import exh.util.isLewd
import exh.util.nullIfBlank
import java.util.Collections import java.util.Collections
import java.util.Comparator import java.util.Comparator
import rx.Observable import rx.Observable
@@ -52,7 +57,10 @@ class LibraryPresenter(
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get() private val downloadManager: DownloadManager = Injekt.get(),
// SY -->
private val customMangaManager: CustomMangaManager = Injekt.get()
// SY <--
) : BasePresenter<LibraryController>() { ) : BasePresenter<LibraryController>() {
private val context = preferences.context private val context = preferences.context
@@ -83,9 +91,26 @@ class LibraryPresenter(
*/ */
private var librarySubscription: Subscription? = null private var librarySubscription: Subscription? = null
// --> EXH // SY -->
val favoritesSync = FavoritesSyncHelper(context) val favoritesSync = FavoritesSyncHelper(context)
// <-- EXH
private var groupType = preferences.groupLibraryBy().get()
private val libraryIsGrouped
get() = groupType != LibraryGroup.UNGROUPED
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
/**
* Relay used to apply the UI update to the last emission of the library.
*/
private val buttonTriggerRelay = BehaviorRelay.create(Unit)
/**
* Relay used to apply the UI update to the last emission of the library.
*/
private val groupingTriggerRelay = BehaviorRelay.create(Unit)
// SY <--
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@@ -101,6 +126,15 @@ class LibraryPresenter(
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.apply { setBadges(mangaMap) } lib.apply { setBadges(mangaMap) }
} }
// SY -->
.combineLatest(buttonTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.apply { setButtons(mangaMap) }
}
.combineLatest(groupingTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
val (map, categories) = applyGrouping(lib.mangaMap, lib.categories)
lib.copy(mangaMap = map, categories = categories)
}
// SY <--
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> .combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
lib.copy(mangaMap = applyFilters(lib.mangaMap)) lib.copy(mangaMap = applyFilters(lib.mangaMap))
} }
@@ -166,6 +200,21 @@ class LibraryPresenter(
return map.mapValues { entry -> entry.value.filter(filterFn) } return map.mapValues { entry -> entry.value.filter(filterFn) }
} }
/**
* Sets the button on each manga.
*
* @param map the map of manga.
*/
private fun setButtons(map: LibraryMap) {
val startReadingButton = preferences.startReadingButton().get()
for ((_, itemList) in map) {
for (item in itemList) {
item.startReadingButton = startReadingButton
}
}
}
// SY <-- // SY <--
/** /**
@@ -283,6 +332,27 @@ class LibraryPresenter(
} }
} }
// SY -->
private fun applyGrouping(map: LibraryMap, categories: List<Category>): Pair<LibraryMap, List<Category>> {
groupType = preferences.groupLibraryBy().get()
var editedCategories: List<Category> = categories
val libraryMangaAsList = map.flatMap { it.value }.distinctBy { it.manga.id }
val items = if (groupType == LibraryGroup.BY_DEFAULT) {
map
} else if (!libraryIsGrouped) {
editedCategories = listOf(Category.create("All").apply { this.id = 0 })
libraryMangaAsList
.groupBy { 0 }
} else {
val (items, customCategories) = getGroupedMangaItems(libraryMangaAsList)
editedCategories = customCategories
items
}
return items to editedCategories
}
// SY <--
/** /**
* Get the categories from the database. * Get the categories from the database.
* *
@@ -320,6 +390,23 @@ class LibraryPresenter(
badgeTriggerRelay.call(Unit) badgeTriggerRelay.call(Unit)
} }
// SY -->
/**
* Requests the library to have buttons toggled.
*/
fun requestButtonsUpdate() {
buttonTriggerRelay.call(Unit)
}
/**
* Requests the library to have groups refreshed.
*/
fun requestGroupsUpdate() {
groupingTriggerRelay.call(Unit)
}
// SY <--
/** /**
* Requests the library to be sorted. * Requests the library to be sorted.
*/ */
@@ -357,7 +444,7 @@ class LibraryPresenter(
launchIO { launchIO {
/* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) { /* SY --> */ val chapters = if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
val chapter = db.getChapters(manga).executeAsBlocking().minBy { it.source_order } val chapter = db.getChapters(manga).executeAsBlocking().minBy { it.source_order }
if (chapter != null) listOf(chapter) else emptyList() if (chapter != null && !chapter.read) listOf(chapter) else emptyList()
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking() } else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
.filter { !it.read } .filter { !it.read }
@@ -366,6 +453,64 @@ class LibraryPresenter(
} }
} }
// SY -->
fun cleanTitles(mangas: List<Manga>) {
mangas.forEach { manga ->
val editedTitle = manga.title.replace("\\[.*?]".toRegex(), "").trim().replace("\\(.*?\\)".toRegex(), "").trim().replace("\\{.*?\\}".toRegex(), "").trim().let {
if (it.contains("|")) {
it.replace(".*\\|".toRegex(), "").trim()
} else {
it
}
}
if (manga.title == editedTitle) return@forEach
val mangaJson = manga.id?.let {
CustomMangaManager.MangaJson(
it,
editedTitle.nullIfBlank(),
(if (manga.author != manga.originalAuthor) manga.author else null),
(if (manga.artist != manga.originalArtist) manga.artist else null),
(if (manga.description != manga.originalDescription) manga.description else null),
(if (manga.genre != manga.originalGenre) manga.getGenres()?.toTypedArray() else null)
)
}
mangaJson?.let {
customMangaManager.saveMangaInfo(it)
}
}
}
// SY <--
/**
* Marks mangas' chapters read status.
*
* @param mangas the list of manga.
*/
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
mangas.forEach { manga ->
launchIO {
val chapters = db.getChapters(manga).executeAsBlocking()
chapters.forEach {
it.read = read
if (!read) {
it.last_page_read = 0
}
}
db.updateChaptersProgress(chapters).executeAsBlocking()
if (preferences.removeAfterMarkedAsRead()) {
deleteChapters(manga, chapters)
}
}
}
}
private fun deleteChapters(manga: Manga, chapters: List<Chapter>) {
sourceManager.get(manga.source)?.let { source ->
downloadManager.deleteChapters(chapters, manga, source)
}
}
/** /**
* Remove the selected manga from the library. * Remove the selected manga from the library.
* *
@@ -410,4 +555,121 @@ class LibraryPresenter(
db.setMangaCategories(mc, mangas) db.setMangaCategories(mc, mangas)
} }
// SY -->
/** Returns first unread chapter of a manga */
fun getFirstUnread(manga: Manga): Chapter? {
val chapters = db.getChapters(manga).executeAsBlocking()
return if (manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
if (chapter?.read == false) chapter else null
} else {
chapters.sortedByDescending { it.source_order }.find { !it.read }
}
}
private fun getGroupedMangaItems(libraryManga: List<LibraryItem>): Pair<LibraryMap, List<Category>> {
val grouping: MutableList<Triple<String, Int, String>> = mutableListOf()
when (groupType) {
LibraryGroup.BY_STATUS -> libraryManga.distinctBy { it.manga.status }.map { it.manga.status }.forEachIndexed { index, status ->
grouping += Triple(status.toString(), index, mapStatus(status))
}
LibraryGroup.BY_SOURCE -> libraryManga.distinctBy { it.manga.source }.map { it.manga.source }.forEachIndexed { index, sourceLong ->
grouping += Triple(sourceLong.toString(), index, sourceManager.getOrStub(sourceLong).name)
}
LibraryGroup.BY_TRACK_STATUS -> {
grouping += Triple("1", 1, context.getString(R.string.reading))
grouping += Triple("2", 2, context.getString(R.string.repeating))
grouping += Triple("3", 3, context.getString(R.string.plan_to_read))
grouping += Triple("4", 4, context.getString(R.string.on_hold))
grouping += Triple("5", 5, context.getString(R.string.completed))
grouping += Triple("6", 6, context.getString(R.string.dropped))
grouping += Triple("7", 7, context.getString(R.string.not_tracked))
}
}
val map: MutableMap<Int, MutableList<LibraryItem>> = mutableMapOf()
libraryManga.forEach { libraryItem ->
when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> {
val status: String = {
val tracks = db.getTracks(libraryItem.manga).executeAsBlocking()
val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id }
}
val service = loggedServices.find { it.id == track?.sync_id }
if (track != null && service != null) {
service.getStatus(track.status)
} else {
"not tracked"
}
}()
val group = grouping.find { it.first == mapTrackingOrder(status) }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
map[7]?.plusAssign(libraryItem) ?: map.put(7, mutableListOf(libraryItem))
}
}
LibraryGroup.BY_SOURCE -> {
val group = grouping.find { it.first.toLongOrNull() == libraryItem.manga.source }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
}
}
else -> {
val group = grouping.find { it.first == libraryItem.manga.status.toString() }
if (group != null) {
map[group.second]?.plusAssign(libraryItem) ?: map.put(group.second, mutableListOf(libraryItem))
} else {
if (grouping.all { it.second != Int.MAX_VALUE }) grouping += Triple(Int.MAX_VALUE.toString(), Int.MAX_VALUE, context.getString(R.string.unknown))
map[Int.MAX_VALUE]?.plusAssign(libraryItem) ?: map.put(Int.MAX_VALUE, mutableListOf(libraryItem))
}
}
}
}
val categories = (
when (groupType) {
LibraryGroup.BY_SOURCE -> grouping.sortedBy { it.third.toLowerCase() }
LibraryGroup.BY_TRACK_STATUS -> grouping.filter { it.second in map.keys }
else -> grouping
}
).map {
val category = Category.create(it.third)
category.id = it.second
category
}
return map to categories
}
private fun mapTrackingOrder(status: String): String {
with(context) {
return when (status) {
getString(R.string.reading), getString(R.string.currently_reading) -> "1"
getString(R.string.repeating) -> "2"
getString(R.string.plan_to_read), getString(R.string.want_to_read) -> "3"
getString(R.string.on_hold), getString(R.string.paused) -> "4"
getString(R.string.completed) -> "5"
getString(R.string.dropped) -> "6"
else -> "7"
}
}
}
private fun mapStatus(status: Int): String {
return context.getString(
when (status) {
SManga.LICENSED -> R.string.licensed
SManga.ONGOING -> R.string.ongoing
SManga.COMPLETED -> R.string.completed
else -> R.string.unknown
}
)
}
// SY <--
} }
@@ -5,6 +5,7 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@@ -25,6 +26,7 @@ class LibrarySettingsSheet(
val filters: Filter val filters: Filter
private val sort: Sort private val sort: Sort
private val display: Display private val display: Display
private val grouping: Grouping
init { init {
filters = Filter(activity) filters = Filter(activity)
@@ -35,18 +37,27 @@ class LibrarySettingsSheet(
display = Display(activity) display = Display(activity)
display.onGroupClicked = onGroupClickListener display.onGroupClicked = onGroupClickListener
grouping = Grouping(activity)
grouping.onGroupClicked = onGroupClickListener
}
fun refreshSort() {
sort.refreshMode()
} }
override fun getTabViews(): List<View> = listOf( override fun getTabViews(): List<View> = listOf(
filters, filters,
sort, sort,
display display,
grouping
) )
override fun getTabTitles(): List<Int> = listOf( override fun getTabTitles(): List<Int> = listOf(
R.string.action_filter, R.string.action_filter,
R.string.action_sort, R.string.action_sort,
R.string.action_display R.string.action_display,
R.string.group
) )
/** /**
@@ -141,6 +152,12 @@ class LibrarySettingsSheet(
setGroups(listOf(SortGroup())) setGroups(listOf(SortGroup()))
} }
fun refreshMode() {
recycler.adapter = null
removeView(recycler)
setGroups(listOf(SortGroup()))
}
inner class SortGroup : Group { inner class SortGroup : Group {
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this) private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
@@ -188,6 +205,9 @@ class LibrarySettingsSheet(
override fun onItemClicked(item: Item) { override fun onItemClicked(item: Item) {
item as Item.MultiStateGroup item as Item.MultiStateGroup
// SY -->
if (item == dragAndDrop && preferences.groupLibraryBy().get() != LibraryGroup.BY_DEFAULT) return
// SY <--
val prevState = item.state val prevState = item.state
item.group.items.forEach { item.group.items.forEach {
@@ -236,7 +256,7 @@ class LibrarySettingsSheet(
Settings(context, attrs) { Settings(context, attrs) {
init { init {
setGroups(listOf(DisplayGroup(), BadgeGroup(), TabsGroup())) setGroups(listOf(DisplayGroup(), BadgeGroup(), /* SY --> */ ButtonsGroup(), /* SY <-- */ TabsGroup()))
} }
inner class DisplayGroup : Group { inner class DisplayGroup : Group {
@@ -300,6 +320,29 @@ class LibrarySettingsSheet(
} }
} }
// SY -->
inner class ButtonsGroup : Group {
private val startReadingButton = Item.CheckboxGroup(R.string.action_start_reading_button, this)
override val header = Item.Header(R.string.buttons_header)
override val items = listOf(startReadingButton)
override val footer = null
override fun initModels() {
startReadingButton.checked = preferences.startReadingButton().get()
}
override fun onItemClicked(item: Item) {
item as Item.CheckboxGroup
item.checked = !item.checked
when (item) {
startReadingButton -> preferences.startReadingButton().set((item.checked))
}
adapter.notifyItemChanged(item)
}
}
// SY <--
inner class TabsGroup : Group { inner class TabsGroup : Group {
private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this) private val showTabs = Item.CheckboxGroup(R.string.action_display_show_tabs, this)
@@ -322,6 +365,80 @@ class LibrarySettingsSheet(
} }
} }
// SY -->
inner class Grouping @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Settings(context, attrs) {
init {
setGroups(listOf(InternalGroup()))
}
inner class InternalGroup : Group {
private val groupItems = mutableListOf<Item.DrawableSelection>()
private val db: DatabaseHelper = Injekt.get()
private val trackManager: TrackManager = Injekt.get()
private val hasCategories = db.getCategories().executeAsBlocking().size != 0
init {
val groupingItems = mutableListOf(
LibraryGroup.BY_DEFAULT,
LibraryGroup.BY_SOURCE,
LibraryGroup.BY_STATUS
)
if (trackManager.hasLoggedServices()) {
groupingItems.add(LibraryGroup.BY_TRACK_STATUS)
}
if (hasCategories) {
groupingItems.add(LibraryGroup.UNGROUPED)
}
groupItems += groupingItems.map { id ->
Item.DrawableSelection(
id,
this,
LibraryGroup.groupTypeStringRes(id, hasCategories),
LibraryGroup.groupTypeDrawableRes(id)
)
}
}
override val header = null
override val items = groupItems
override val footer = null
override fun initModels() {
val groupType = preferences.groupLibraryBy().get()
items.forEach {
it.state = if (it.id == groupType) {
Item.DrawableSelection.SELECTED
} else {
Item.DrawableSelection.NOT_SELECTED
}
}
}
override fun onItemClicked(item: Item) {
item as Item.DrawableSelection
if (item.id != LibraryGroup.BY_DEFAULT && preferences.librarySortingMode().get() == LibrarySort.DRAG_AND_DROP) {
preferences.librarySortingMode().set(LibrarySort.ALPHA)
preferences.librarySortingAscending().set(true)
refreshSort()
}
item.group.items.forEach {
(it as Item.DrawableSelection).state =
Item.DrawableSelection.NOT_SELECTED
}
item.state = Item.DrawableSelection.SELECTED
preferences.groupLibraryBy().set(item.id)
item.group.items.forEach { adapter.notifyItemChanged(it) }
}
}
}
// SY <--
open inner class Settings(context: Context, attrs: AttributeSet?) : open inner class Settings(context: Context, attrs: AttributeSet?) :
ExtendedNavigationView(context, attrs) { ExtendedNavigationView(context, attrs) {
@@ -5,14 +5,12 @@ import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.preference.PreferenceDialogController
import com.bluelinelabs.conductor.Conductor import com.bluelinelabs.conductor.Conductor
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@@ -20,11 +18,10 @@ import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.MainActivityBinding import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
@@ -33,6 +30,7 @@ import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
@@ -44,16 +42,9 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.snack
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXHMigrations import exh.EXHMigrations
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HITOMI_SOURCE_ID
import exh.NHENTAI_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.eh.EHentaiUpdateWorker import exh.eh.EHentaiUpdateWorker
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.uconfig.WarnConfigureDialogController import exh.uconfig.WarnConfigureDialogController
@@ -64,7 +55,6 @@ import kotlinx.android.synthetic.main.main_activity.appbar
import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.android.synthetic.main.main_activity.tabs
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
class MainActivity : BaseActivity<MainActivityBinding>() { class MainActivity : BaseActivity<MainActivityBinding>() {
@@ -187,13 +177,13 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
// Show changelog prompt on update // Show changelog prompt on update
// TODO // TODO
// if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) { // if (Migrations.upgrade(preferences) && !BuildConfig.DEBUG) {
// showUpdateInfoSnackbar() // WhatsNewDialogController().showDialog(router)
// } // }
// EXH --> // EXH -->
// Perform EXH specific migrations // Perform EXH specific migrations
if (EXHMigrations.upgrade(preferences)) { if (EXHMigrations.upgrade(preferences)) {
ChangelogDialogController().showDialog(router) WhatsNewDialogController().showDialog(router)
} }
// EXH <-- // EXH <--
@@ -219,27 +209,11 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) { if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
} }
if (PERV_EDEN_EN_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_EN_SOURCE_ID
}
if (PERV_EDEN_IT_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_IT_SOURCE_ID
}
if (NHENTAI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += NHENTAI_SOURCE_ID
}
if (HITOMI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += HITOMI_SOURCE_ID
}
if (EIGHTMUSES_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += EIGHTMUSES_SOURCE_ID
}
} }
// SY --> // SY -->
setExtensionsBadge() preferences.extensionUpdatesCount()
preferences.extensionUpdatesCount().asFlow() .asImmediateFlow { setExtensionsBadge() }
.onEach { setExtensionsBadge() }
.launchIn(scope) .launchIn(scope)
} }
@@ -401,6 +375,9 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
if (from is DialogController || to is DialogController) { if (from is DialogController || to is DialogController) {
return return
} }
if (from is PreferenceDialogController || to is PreferenceDialogController) {
return
}
supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1) supportActionBar?.setDisplayHomeAsUpEnabled(router.backstackSize != 1)
@@ -435,10 +412,16 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
to.configureFab(binding.rootFab) to.configureFab(binding.rootFab)
} }
if (to is NoToolbarElevationController) { when (to) {
binding.appbar.disableElevation() is NoToolbarElevationController -> {
} else { binding.appbar.disableElevation()
binding.appbar.enableElevation() }
is ToolbarLiftOnScrollController -> {
binding.appbar.enableElevation(true)
}
else -> {
binding.appbar.enableElevation(false)
}
} }
} }
@@ -460,32 +443,6 @@ class MainActivity : BaseActivity<MainActivityBinding>() {
} }
} }
private fun showUpdateInfoSnackbar() {
val snack = binding.rootCoordinator.snack(
getString(R.string.updated_version, BuildConfig.VERSION_NAME),
Snackbar.LENGTH_INDEFINITE
) {
setAction(R.string.whats_new) {
val url = "https://github.com/inorichi/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
}
// Ensure the snackbar sits above the bottom nav
view.updateLayoutParams<CoordinatorLayout.LayoutParams> {
anchorId = binding.bottomNav.id
anchorGravity = Gravity.TOP
gravity = Gravity.TOP
}
}
// Manually handle dismiss delay since Snackbar.LENGTH_LONG is a too short
launchIO {
delay(10000)
snack.dismiss()
}
}
companion object { companion object {
// Shortcut actions // Shortcut actions
const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY" const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
@@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import exh.syDebugVersion import exh.syDebugVersion
import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView import it.gmariotti.changelibs.library.view.ChangeLogRecyclerView
class ChangelogDialogController : DialogController() { class WhatsNewDialogController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!! val activity = activity!!
@@ -100,7 +100,7 @@ class EditMangaDialog : DialogController {
view.manga_author.append(manga.author ?: "") view.manga_author.append(manga.author ?: "")
view.manga_artist.append(manga.artist ?: "") view.manga_artist.append(manga.artist ?: "")
view.manga_description.append(manga.description ?: "") view.manga_description.append(manga.description ?: "")
view.manga_genres_tags.setChips(manga.genre?.split(",")?.map { it.trim() } ?: emptyList()) view.manga_genres_tags.setChips(manga.getGenres())
} else { } else {
if (manga.title != manga.originalTitle) { if (manga.title != manga.originalTitle) {
view.title.append(manga.title) view.title.append(manga.title)
@@ -114,7 +114,7 @@ class EditMangaDialog : DialogController {
if (manga.description != manga.originalDescription) { if (manga.description != manga.originalDescription) {
view.manga_description.append(manga.description ?: "") view.manga_description.append(manga.description ?: "")
} }
view.manga_genres_tags.setChips(manga.genre?.split(",")?.map { it.trim() } ?: emptyList()) view.manga_genres_tags.setChips(manga.getGenres())
view.title.hint = "${resources?.getString(R.string.title)}: ${manga.originalTitle}" view.title.hint = "${resources?.getString(R.string.title)}: ${manga.originalTitle}"
if (manga.originalAuthor != null) { if (manga.originalAuthor != null) {
@@ -147,7 +147,7 @@ class EditMangaDialog : DialogController {
if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setChips( if (manga.genre.isNullOrBlank() || manga.source == LocalSource.ID) dialogView?.manga_genres_tags?.setChips(
emptyList() emptyList()
) )
else dialogView?.manga_genres_tags?.setChips(manga.originalGenre?.split(", ")) else dialogView?.manga_genres_tags?.setChips(manga.getOriginalGenres())
} }
fun updateCover(uri: Uri) { fun updateCover(uri: Uri) {
@@ -14,6 +14,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -40,6 +43,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource.Companion.getLewdSource import eu.kanade.tachiyomi.source.online.LewdSource.Companion.getLewdSource
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationController
import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController
@@ -93,6 +97,7 @@ import uy.kohesive.injekt.injectLazy
class MangaController : class MangaController :
NucleusController<MangaControllerBinding, MangaPresenter>, NucleusController<MangaControllerBinding, MangaPresenter>,
ToolbarLiftOnScrollController,
FabController, FabController,
ActionMode.Callback, ActionMode.Callback,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
@@ -137,23 +142,25 @@ class MangaController :
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
private val toolbarTextColor by lazy { view!!.context.getResourceColor(R.attr.colorOnPrimary) } private val toolbarTextColor by lazy { view!!.context.getResourceColor(R.attr.colorOnPrimary) }
private var toolbarTextAlpha = 255
private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null private var mangaInfoAdapter: MangaInfoHeaderAdapter? = null
// SY >--
private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null private var mangaInfoItemAdapter: MangaInfoItemAdapter? = null
private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null private var mangaInfoButtonsAdapter: MangaInfoButtonsAdapter? = null
private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null private var mangaMetaInfoAdapter: RecyclerView.Adapter<*>? = null
// SY <--
private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null private var chaptersHeaderAdapter: MangaChaptersHeaderAdapter? = null
private var chaptersAdapter: ChaptersAdapter? = null private var chaptersAdapter: ChaptersAdapter? = null
/** // Sheet containing filter/sort/display items.
* Sheet containing filter/sort/display items.
*/
private var settingsSheet: ChaptersSettingsSheet? = null private var settingsSheet: ChaptersSettingsSheet? = null
private var actionFab: ExtendedFloatingActionButton? = null private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
// Snackbar to add manga to library after downloading chapter(s)
private var addSnackbar: Snackbar? = null
/** /**
* Action mode for multiple selection. * Action mode for multiple selection.
*/ */
@@ -183,6 +190,19 @@ class MangaController :
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun getTitle(): String? {
return manga?.title
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
// Hide toolbar title on enter
if (type.isEnter) {
updateToolbarTitleAlpha()
}
}
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeEnded(handler, type) super.onChangeEnded(handler, type)
if (manga == null || source == null) { if (manga == null || source == null) {
@@ -210,6 +230,7 @@ class MangaController :
val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf() val adapters: MutableList<RecyclerView.Adapter<out RecyclerView.ViewHolder>?> = mutableListOf()
// Init RecyclerView and adapter // Init RecyclerView and adapter
// SY -->
mangaInfoAdapter = MangaInfoHeaderAdapter(this) mangaInfoAdapter = MangaInfoHeaderAdapter(this)
adapters += mangaInfoAdapter adapters += mangaInfoAdapter
@@ -238,6 +259,7 @@ class MangaController :
binding.recycler.adapter = ConcatAdapter(adapters) binding.recycler.adapter = ConcatAdapter(adapters)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && thisSourceAsLewdSource != null) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || thisSourceAsLewdSource != null) 3 else 2)) binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && thisSourceAsLewdSource != null) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || thisSourceAsLewdSource != null) 3 else 2))
// SY <--
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
chaptersAdapter?.fastScroller = binding.fastScroller chaptersAdapter?.fastScroller = binding.fastScroller
@@ -252,7 +274,6 @@ class MangaController :
// Delayed in case we need to jump to chapters // Delayed in case we need to jump to chapters
binding.recycler.post { binding.recycler.post {
updateToolbarTitleAlpha() updateToolbarTitleAlpha()
setTitle(manga?.title)
} }
} }
@@ -291,18 +312,14 @@ class MangaController :
else -> min(binding.recycler.computeVerticalScrollOffset(), 255) else -> min(binding.recycler.computeVerticalScrollOffset(), 255)
} }
if (calculatedAlpha != toolbarTextAlpha) { activity?.toolbar?.setTitleTextColor(
toolbarTextAlpha = calculatedAlpha Color.argb(
calculatedAlpha,
activity?.toolbar?.setTitleTextColor( toolbarTextColor.red,
Color.argb( toolbarTextColor.green,
toolbarTextAlpha, toolbarTextColor.blue
Color.red(toolbarTextColor),
Color.green(toolbarTextColor),
Color.blue(toolbarTextColor)
)
) )
} )
} }
private fun updateFilterIconState() { private fun updateFilterIconState() {
@@ -354,6 +371,12 @@ class MangaController :
chaptersHeaderAdapter = null chaptersHeaderAdapter = null
chaptersAdapter = null chaptersAdapter = null
settingsSheet = null settingsSheet = null
// SY -->
mangaInfoButtonsAdapter = null
mangaInfoItemAdapter = null
mangaMetaInfoAdapter = null
// SY <--
addSnackbar?.dismiss()
updateToolbarTitleAlpha(255) updateToolbarTitleAlpha(255)
super.onDestroyView(view) super.onDestroyView(view)
} }
@@ -514,12 +537,10 @@ class MangaController :
if (manga.favorite) { if (manga.favorite) {
toggleFavorite() toggleFavorite()
activity?.toast(activity?.getString(R.string.manga_removed_library)) activity?.toast(activity?.getString(R.string.manga_removed_library))
activity?.invalidateOptionsMenu()
} else { } else {
addToLibrary(manga) addToLibrary(manga)
} }
// Update menu to show migrate option
activity?.invalidateOptionsMenu()
} }
fun onTrackingClick() { fun onTrackingClick() {
@@ -537,6 +558,7 @@ class MangaController :
toggleFavorite() toggleFavorite()
presenter.moveMangaToCategory(manga, defaultCategory) presenter.moveMangaToCategory(manga, defaultCategory)
activity?.toast(activity?.getString(R.string.manga_added_library)) activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
} }
// Automatic 'Default' or no categories // Automatic 'Default' or no categories
@@ -544,6 +566,7 @@ class MangaController :
toggleFavorite() toggleFavorite()
presenter.moveMangaToCategory(manga, null) presenter.moveMangaToCategory(manga, null)
activity?.toast(activity?.getString(R.string.manga_added_library)) activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
} }
// Choose a category // Choose a category
@@ -667,6 +690,7 @@ class MangaController :
if (!manga.favorite) { if (!manga.favorite) {
toggleFavorite() toggleFavorite()
activity?.toast(activity?.getString(R.string.manga_added_library)) activity?.toast(activity?.getString(R.string.manga_added_library))
activity?.invalidateOptionsMenu()
} }
presenter.moveMangaToCategories(manga, categories) presenter.moveMangaToCategories(manga, categories)
@@ -1050,7 +1074,7 @@ class MangaController :
val manga = presenter.manga val manga = presenter.manga
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)
if (view != null && !manga.favorite) { if (view != null && !manga.favorite) {
binding.recycler.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) { addSnackbar = activity!!.root_coordinator?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
setAction(R.string.action_add) { setAction(R.string.action_add) {
addToLibrary(manga) addToLibrary(manga)
} }
@@ -265,12 +265,12 @@ class MangaPresenter(
manga.author = author?.trimOrNull() manga.author = author?.trimOrNull()
manga.artist = artist?.trimOrNull() manga.artist = artist?.trimOrNull()
manga.description = description?.trimOrNull() manga.description = description?.trimOrNull()
val tagsString = tags?.joinToString(", ") val tagsString = tags?.joinToString()
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim() manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
LocalSource(downloadManager.context).updateMangaInfo(manga) LocalSource(downloadManager.context).updateMangaInfo(manga)
db.updateMangaInfo(manga).executeAsBlocking() db.updateMangaInfo(manga).executeAsBlocking()
} else { } else {
val genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.genre) { val genre = if (!tags.isNullOrEmpty() && tags.joinToString() != manga.genre) {
tags.toTypedArray() tags.toTypedArray()
} else { } else {
null null
@@ -716,7 +716,7 @@ class MangaPresenter(
} }
private fun downloadNewChapters(chapters: List<Chapter>) { private fun downloadNewChapters(chapters: List<Chapter>) {
if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences)) return if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences) || source.isEhBasedSource()) return
downloadChapters(chapters) downloadChapters(chapters)
} }
@@ -726,8 +726,14 @@ class MangaPresenter(
* @param chapters the chapters to delete. * @param chapters the chapters to delete.
*/ */
private fun deleteChaptersInternal(chapters: List<ChapterItem>) { private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
downloadManager.deleteChapters(chapters, manga, source) val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
chapters.forEach { chapters.filterNot { it.bookmark }
} else {
chapters
}
downloadManager.deleteChapters(filteredChapters, manga, source)
filteredChapters.forEach {
it.status = Download.NOT_DOWNLOADED it.status = Download.NOT_DOWNLOADED
it.download = null it.download = null
} }
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateResult import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.data.updater.UpdaterService import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.main.ChangelogDialogController import eu.kanade.tachiyomi.ui.main.WhatsNewDialogController
import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString import eu.kanade.tachiyomi.util.lang.toDateTimestampString
@@ -72,7 +72,7 @@ class AboutController : SettingsController() {
onClick { onClick {
// SY --> // SY -->
ChangelogDialogController().showDialog(router) WhatsNewDialogController().showDialog(router)
// SY <-- // SY <--
} }
} }
@@ -901,10 +901,12 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
} }
.launchIn(scope) .launchIn(scope)
preferences.readerTheme().asFlow() // SY -->
/*preferences.readerTheme().asFlow()
.drop(1) // We only care about updates .drop(1) // We only care about updates
.onEach { recreate() } .onEach { recreate() }
.launchIn(scope) .launchIn(scope)*/
// SY <--
preferences.showPageNumber().asFlow() preferences.showPageNumber().asFlow()
.onEach { setPageNumberVisibility(it) } .onEach { setPageNumberVisibility(it) }
@@ -27,6 +27,8 @@ import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.updateCoverLastModified import eu.kanade.tachiyomi.util.updateCoverLastModified
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.util.defaultReaderType import exh.util.defaultReaderType
import java.io.File import java.io.File
import java.util.Date import java.util.Date
@@ -360,6 +362,16 @@ class ReaderPresenter(
selectedChapter.chapter.last_page_read = page.index selectedChapter.chapter.last_page_read = page.index
if (selectedChapter.pages?.lastIndex == page.index) { if (selectedChapter.pages?.lastIndex == page.index) {
selectedChapter.chapter.read = true selectedChapter.chapter.read = true
// SY -->
if (manga?.source == EH_SOURCE_ID || manga?.source == EXH_SOURCE_ID) {
chapterList
.filter { it.chapter.source_order > selectedChapter.chapter.source_order }
.onEach {
it.chapter.read = true
saveChapterProgress(it)
}
}
// SY <--
updateTrackChapterRead(selectedChapter) updateTrackChapterRead(selectedChapter)
deleteChapterIfNeeded(selectedChapter) deleteChapterIfNeeded(selectedChapter)
} }
@@ -104,6 +104,9 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon()) binding.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values) binding.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
// SY -->
binding.zoomOutWebtoon.bindToPreference(preferences.webtoonEnableZoomOut())
// SY <--
} }
/** /**
@@ -1,12 +1,15 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.graphics.BitmapFactory
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerPageHolder
import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.system.ImageUtil
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import java.util.concurrent.PriorityBlockingQueue import java.util.concurrent.PriorityBlockingQueue
@@ -258,6 +261,18 @@ class HttpPageLoader(
} }
} }
.doOnNext { .doOnNext {
// SY -->
val readerTheme = prefs.readerTheme().get()
if (readerTheme >= 3) {
val stream = chapterCache.getImageFile(imageUrl).inputStream()
val image = BitmapFactory.decodeStream(stream)
page.bg = ImageUtil.autoSetBackground(
image, readerTheme == 2, prefs.context
)
page.bgType = PagerPageHolder.getBGType(readerTheme, prefs.context)
stream.close()
}
// SY <--
page.stream = { chapterCache.getImageFile(imageUrl).inputStream() } page.stream = { chapterCache.getImageFile(imageUrl).inputStream() }
page.status = Page.READY page.status = Page.READY
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.model package eu.kanade.tachiyomi.ui.reader.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import java.io.InputStream import java.io.InputStream
@@ -7,7 +8,12 @@ class ReaderPage(
index: Int, index: Int,
url: String = "", url: String = "",
imageUrl: String? = null, imageUrl: String? = null,
// SY -->
var bg: Drawable? = null,
var bgType: Int? = null,
// SY <--
var stream: (() -> InputStream)? = null var stream: (() -> InputStream)? = null
) : Page(index, url, imageUrl, null) { ) : Page(index, url, imageUrl, null) {
lateinit var chapter: ReaderChapter lateinit var chapter: ReaderChapter
@@ -20,6 +20,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
var imageCropBorders = false var imageCropBorders = false
private set private set
// SY -->
var readerTheme = 0
private set
// SY <--
init { init {
preferences.imageScaleType() preferences.imageScaleType()
.register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() }) .register({ imageScaleType = it }, { imagePropertyChangedListener?.invoke() })
@@ -29,6 +34,11 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
preferences.cropBorders() preferences.cropBorders()
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
// SY -->
preferences.readerTheme()
.register({ readerTheme = it }, { imagePropertyChangedListener?.invoke() })
// SY <--
} }
private fun zoomTypeFromPreference(value: Int) { private fun zoomTypeFromPreference(value: Int) {
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.PointF import android.graphics.PointF
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.GestureDetector import android.view.GestureDetector
@@ -27,20 +29,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.github.chrisbanes.photoview.PhotoView import com.github.chrisbanes.photoview.PhotoView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressBar
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig.ZoomType
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.widget.ViewPagerAdapter import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import exh.util.isInNightMode
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
/** /**
* View of the ViewPager that contains a page of a chapter. * View of the ViewPager that contains a page of a chapter.
@@ -242,7 +250,34 @@ class PagerPageHolder(
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { isAnimated -> .doOnNext { isAnimated ->
if (!isAnimated) { if (!isAnimated) {
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!)) // SY -->
if (viewer.config.readerTheme >= 3) {
val imageView = initSubsamplingImageView()
if (page.bg != null && page.bgType == getBGType(
viewer.config.readerTheme,
context
)
) {
imageView.setImage(ImageSource.inputStream(openStream!!))
imageView.background = page.bg
}
// if the user switches to automatic when pages are already cached, the bg needs to be loaded
else {
val bytesArray = openStream!!.readBytes()
val bytesStream = bytesArray.inputStream()
imageView.setImage(ImageSource.inputStream(bytesStream))
bytesStream.close()
launchUI {
imageView.background = setBG(bytesArray)
page.bg = imageView.background
page.bgType = getBGType(viewer.config.readerTheme, context)
}
}
} else {
initSubsamplingImageView().setImage(ImageSource.inputStream(openStream!!))
}
// SY <--
} else { } else {
initImageView().setImage(openStream!!) initImageView().setImage(openStream!!)
} }
@@ -253,6 +288,20 @@ class PagerPageHolder(
.subscribe({}, {}) .subscribe({}, {})
} }
// SY -->
private suspend fun setBG(bytesArray: ByteArray): Drawable {
return withContext(Dispatchers.Default) {
val preferences by injectLazy<PreferencesHelper>()
ImageUtil.autoSetBackground(
BitmapFactory.decodeByteArray(
bytesArray, 0, bytesArray.size
),
preferences.readerTheme().get() == 3, context
)
}
}
// SY <--
/** /**
* Called when the page has an error. * Called when the page has an error.
*/ */
@@ -464,4 +513,14 @@ class PagerPageHolder(
}) })
.into(this) .into(this)
} }
// SY -->
companion object {
fun getBGType(readerTheme: Int, context: Context): Int {
return if (readerTheme == 3) {
if (context.isInNightMode()) 2 else 1
} else 0
}
}
// SY <--
} }
@@ -80,29 +80,38 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
isIdle = state == ViewPager.SCROLL_STATE_IDLE isIdle = state == ViewPager.SCROLL_STATE_IDLE
} }
}) })
pager.tapListener = { event -> pager.tapListener = f@{ event ->
if (!config.tappingEnabled) {
activity.toggleMenu()
return@f
}
val positionX = event.x
val positionY = event.y
val topSideTap = positionY < pager.height * 0.25f
val bottomSideTap = positionY > pager.height * 0.75f
val leftSideTap = positionX < pager.width * 0.33f
val rightSideTap = positionX > pager.width * 0.66f
val invertMode = config.tappingInverted val invertMode = config.tappingInverted
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
if (this is VerticalPagerViewer) { if (this is VerticalPagerViewer) {
val positionY = event.y
val tappingInverted = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
val topSideTap = positionY < pager.height * 0.33f && config.tappingEnabled
val bottomSideTap = positionY > pager.height * 0.66f && config.tappingEnabled
when { when {
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> moveLeft() topSideTap && !invertVertical || bottomSideTap && invertVertical -> moveLeft()
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> moveRight() bottomSideTap && !invertVertical || topSideTap && invertVertical -> moveRight()
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
else -> activity.toggleMenu() else -> activity.toggleMenu()
} }
} else { } else {
val positionX = event.x
val tappingInverted = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
val leftSideTap = positionX < pager.width * 0.33f && config.tappingEnabled
val rightSideTap = positionX > pager.width * 0.66f && config.tappingEnabled
when { when {
leftSideTap && !tappingInverted || rightSideTap && tappingInverted -> moveLeft() leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> moveLeft()
rightSideTap && !tappingInverted || leftSideTap && tappingInverted -> moveRight() rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> moveRight()
else -> activity.toggleMenu() else -> activity.toggleMenu()
} }
} }
@@ -16,11 +16,21 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) : ViewerConfi
var sidePadding = 0 var sidePadding = 0
private set private set
// SY -->
var enableZoomOut = false
private set
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
// SY <--
init { init {
preferences.cropBordersWebtoon() preferences.cropBordersWebtoon()
.register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() }) .register({ imageCropBorders = it }, { imagePropertyChangedListener?.invoke() })
preferences.webtoonSidePadding() preferences.webtoonSidePadding()
.register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() }) .register({ sidePadding = it }, { imagePropertyChangedListener?.invoke() })
// SY -->
preferences.webtoonEnableZoomOut()
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
// SY <--
} }
} }
@@ -25,6 +25,14 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
*/ */
private val flingDetector = GestureDetector(context, FlingListener()) private val flingDetector = GestureDetector(context, FlingListener())
// SY -->
var enableZoomOut = false
set(value) {
field = value
recycler?.canZoomOut = value
}
// SY <--
/** /**
* Recycler view added in this frame. * Recycler view added in this frame.
*/ */
@@ -33,6 +33,18 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
private var firstVisibleItemPosition = 0 private var firstVisibleItemPosition = 0
private var lastVisibleItemPosition = 0 private var lastVisibleItemPosition = 0
private var currentScale = DEFAULT_RATE private var currentScale = DEFAULT_RATE
// SY -->
var canZoomOut = false
set(value) {
field = value
if (!value) {
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
}
}
private val minRate
get() = if (canZoomOut) MIN_RATE else DEFAULT_RATE
// SY <--
private val listener = GestureListener() private val listener = GestureListener()
private val detector = Detector() private val detector = Detector()
@@ -163,7 +175,9 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
fun onScale(scaleFactor: Float) { fun onScale(scaleFactor: Float) {
currentScale *= scaleFactor currentScale *= scaleFactor
currentScale = currentScale.coerceIn( currentScale = currentScale.coerceIn(
MIN_RATE, // SY -->
minRate,
// SY <--
MAX_SCALE_RATE MAX_SCALE_RATE
) )
@@ -190,8 +204,8 @@ open class WebtoonRecyclerView @JvmOverloads constructor(
} }
fun onScaleEnd() { fun onScaleEnd() {
if (scaleX < MIN_RATE) { if (scaleX < /* SY --> */ minRate /* SY <-- */) {
zoom(currentScale, MIN_RATE, x, 0f, y, 0f) zoom(currentScale, /* SY --> */ minRate /* SY <-- */, x, 0f, y, 0f)
} }
} }
@@ -93,17 +93,30 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
} }
} }
}) })
recycler.tapListener = { event -> recycler.tapListener = f@{ event ->
val positionY = event.rawY if (!config.tappingEnabled) {
val invertMode = config.tappingInverted activity.toggleMenu()
val topSideTap = positionY < recycler.height * 0.33f && config.tappingEnabled return@f
val bottomSideTap = positionY > recycler.height * 0.66f && config.tappingEnabled }
val tappingInverted = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH val positionX = event.rawX
val positionY = event.rawY
val topSideTap = positionY < recycler.height * 0.25f
val bottomSideTap = positionY > recycler.height * 0.75f
val leftSideTap = positionX < recycler.width * 0.33f
val rightSideTap = positionX > recycler.width * 0.66f
val invertMode = config.tappingInverted
val invertVertical = invertMode == TappingInvertMode.VERTICAL || invertMode == TappingInvertMode.BOTH
val invertHorizontal = invertMode == TappingInvertMode.HORIZONTAL || invertMode == TappingInvertMode.BOTH
when { when {
topSideTap && !tappingInverted || bottomSideTap && tappingInverted -> scrollUp() topSideTap && !invertVertical || bottomSideTap && invertVertical -> scrollUp()
bottomSideTap && !tappingInverted || topSideTap && tappingInverted -> scrollDown() bottomSideTap && !invertVertical || topSideTap && invertVertical -> scrollDown()
leftSideTap && !invertHorizontal || rightSideTap && invertHorizontal -> scrollUp()
rightSideTap && !invertHorizontal || leftSideTap && invertHorizontal -> scrollDown()
else -> activity.toggleMenu() else -> activity.toggleMenu()
} }
} }
@@ -126,6 +139,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
refreshAdapter() refreshAdapter()
} }
// SY -->
config.zoomPropertyChangedListener = {
frame.enableZoomOut = it
}
// SY <--
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
frame.addView(recycler) frame.addView(recycler)
} }
@@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
@@ -14,13 +15,16 @@ import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.onChange import eu.kanade.tachiyomi.util.preference.onChange
@@ -33,18 +37,20 @@ import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HITOMI_SOURCE_ID
import exh.NHENTAI_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.debug.SettingsDebugController import exh.debug.SettingsDebugController
import exh.log.EHLogLevel import exh.log.EHLogLevel
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SettingsAdvancedController : SettingsController() { class SettingsAdvancedController : SettingsController() {
@@ -141,6 +147,17 @@ class SettingsAdvancedController : SettingsController() {
} }
// --> EXH // --> EXH
preferenceCategory {
titleRes = R.string.group_downloader
preference {
titleRes = R.string.clean_up_downloaded_chapters
summaryRes = R.string.delete_unused_chapters
onClick { cleanupDownloads() }
}
}
preferenceCategory { preferenceCategory {
titleRes = R.string.developer_tools titleRes = R.string.developer_tools
isPersistent = false isPersistent = false
@@ -159,21 +176,6 @@ class SettingsAdvancedController : SettingsController() {
if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) { if (EXH_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES += EXH_SOURCE_ID
} }
if (PERV_EDEN_EN_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_EN_SOURCE_ID
}
if (PERV_EDEN_IT_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += PERV_EDEN_IT_SOURCE_ID
}
if (NHENTAI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += NHENTAI_SOURCE_ID
}
if (HITOMI_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += HITOMI_SOURCE_ID
}
if (EIGHTMUSES_SOURCE_ID !in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES += EIGHTMUSES_SOURCE_ID
}
} else { } else {
if (EH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) { if (EH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= EH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES -= EH_SOURCE_ID
@@ -181,21 +183,6 @@ class SettingsAdvancedController : SettingsController() {
if (EXH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) { if (EXH_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= EXH_SOURCE_ID BlacklistedSources.HIDDEN_SOURCES -= EXH_SOURCE_ID
} }
if (PERV_EDEN_EN_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= PERV_EDEN_EN_SOURCE_ID
}
if (PERV_EDEN_IT_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= PERV_EDEN_IT_SOURCE_ID
}
if (NHENTAI_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= NHENTAI_SOURCE_ID
}
if (HITOMI_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= HITOMI_SOURCE_ID
}
if (EIGHTMUSES_SOURCE_ID in BlacklistedSources.HIDDEN_SOURCES) {
BlacklistedSources.HIDDEN_SOURCES -= EIGHTMUSES_SOURCE_ID
}
} }
true true
} }
@@ -237,6 +224,35 @@ class SettingsAdvancedController : SettingsController() {
// <-- EXH // <-- EXH
} }
// SY -->
private fun cleanupDownloads() {
if (job?.isActive == true) return
activity?.toast(R.string.starting_cleanup)
job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) {
val mangaList = db.getMangas().executeAsBlocking()
val sourceManager: SourceManager = Injekt.get()
val downloadManager: DownloadManager = Injekt.get()
var foldersCleared = 0
mangaList.forEach { manga ->
val chapterList = db.getChapters(manga).executeAsBlocking()
val source = sourceManager.getOrStub(manga.source)
foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source)
}
launchUI {
val activity = activity ?: return@launchUI
val cleanupString =
if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup)
else resources!!.getQuantityString(
R.plurals.cleanup_done,
foldersCleared,
foldersCleared
)
activity.toast(cleanupString, Toast.LENGTH_LONG)
}
}
}
// SY <--
private fun clearChapterCache() { private fun clearChapterCache() {
if (activity == null) return if (activity == null) return
val files = chapterCache.cacheDir.listFiles() ?: return val files = chapterCache.cacheDir.listFiles() ?: return
@@ -281,5 +297,7 @@ class SettingsAdvancedController : SettingsController() {
private companion object { private companion object {
const val CLEAR_CACHE_KEY = "pref_clear_cache_key" const val CLEAR_CACHE_KEY = "pref_clear_cache_key"
private var job: Job? = null
} }
} }
@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.data.backup.BackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
@@ -37,8 +36,6 @@ import eu.kanade.tachiyomi.util.system.getFilePicker
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SettingsBackupController : SettingsController() { class SettingsBackupController : SettingsController() {
@@ -258,16 +255,12 @@ class SettingsBackupController : SettingsController() {
return try { return try {
var message = activity.getString(R.string.backup_restore_content) var message = activity.getString(R.string.backup_restore_content)
val sources = BackupRestoreValidator.validate(activity, uri) val results = BackupRestoreValidator.validate(activity, uri)
if (sources.isNotEmpty()) { if (results.missingSources.isNotEmpty()) {
val sourceManager = Injekt.get<SourceManager>() message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
val missingSources = sources }
.filter { sourceManager.get(it.key) == null } if (results.missingTrackers.isNotEmpty()) {
.values message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}"
.sorted()
if (missingSources.isNotEmpty()) {
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${missingSources.joinToString("\n") { "- $it" }}"
}
} }
MaterialDialog(activity) MaterialDialog(activity)
@@ -65,7 +65,7 @@ class SettingsDownloadController : SettingsController() {
defaultValue = true defaultValue = true
} }
preferenceCategory { preferenceCategory {
titleRes = R.string.pref_remove_after_read titleRes = R.string.pref_category_delete_chapters
switchPreference { switchPreference {
key = Keys.removeAfterMarkedAsRead key = Keys.removeAfterMarkedAsRead
@@ -84,6 +84,11 @@ class SettingsDownloadController : SettingsController() {
defaultValue = "-1" defaultValue = "-1"
summary = "%s" summary = "%s"
} }
switchPreference {
key = Keys.removeBookmarkedChapters
titleRes = R.string.pref_remove_bookmarked_chapters
defaultValue = false
}
} }
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeAsBlocking()
@@ -205,7 +205,6 @@ class SettingsGeneralController : SettingsController() {
"sr", "sr",
"sv", "sv",
"th", "th",
"tl",
"tr", "tr",
"uk", "uk",
"ur-rPK", "ur-rPK",
@@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
/**
* hitomi.la Settings fragment
*/
class SettingsHlController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.pref_category_hl
switchPreference {
titleRes = R.string.high_quality_thumbnails
summaryRes = R.string.high_quality_thumbnails_summary
key = PreferenceKeys.eh_hl_useHighQualityThumbs
defaultValue = false
}
switchPreference {
titleRes = R.string.always_download_webp
summaryOn = context.getString(R.string.always_download_webp_summary_on)
summaryOff = context.getString(R.string.always_download_webp_summary_off)
key = PreferenceKeys.hitomiAlwaysWebp
defaultValue = true
}
}
}
@@ -65,6 +65,12 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.pref_category_security titleRes = R.string.pref_category_security
onClick { navigateTo(SettingsSecurityController()) } onClick { navigateTo(SettingsSecurityController()) }
} }
// preference {
// iconRes = R.drawable.ic_outline_people_alt_24dp
// iconTint = tintColor
// titleRes = R.string.pref_category_parental_controls
// onClick { navigateTo(SettingsParentalControlsController()) }
// }
// SY --> // SY -->
if (preferences.eh_isHentaiEnabled().get()) { if (preferences.eh_isHentaiEnabled().get()) {
preference { preference {
@@ -73,18 +79,6 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.pref_category_eh titleRes = R.string.pref_category_eh
onClick { navigateTo(SettingsEhController()) } onClick { navigateTo(SettingsEhController()) }
} }
preference {
iconRes = R.drawable.eh_ic_nhlogo_color
iconTint = tintColor
titleRes = R.string.pref_category_nh
onClick { navigateTo(SettingsNhController()) }
}
preference {
iconRes = R.drawable.eh_ic_hllogo
iconTint = tintColor
titleRes = R.string.pref_category_hl
onClick { navigateTo(SettingsHlController()) }
}
} }
// SY <-- // SY <--
preference { preference {
@@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
/**
* nhentai Settings fragment
*/
class SettingsNhController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.pref_category_nh
switchPreference {
titleRes = R.string.high_quality_thumbnails
summaryRes = R.string.high_quality_thumbnails_summary
key = PreferenceKeys.eh_nh_useHighQualityThumbs
defaultValue = false
}
}
}
@@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.titleRes
class SettingsParentalControlsController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.pref_category_parental_controls
listPreference {
key = Keys.allowNsfwSource
titleRes = R.string.pref_allow_nsfw_sources
entriesRes = arrayOf(
R.string.pref_allow_nsfw_sources_allowed,
R.string.pref_allow_nsfw_sources_allowed_multisource,
R.string.pref_allow_nsfw_sources_blocked
)
entryValues = arrayOf(
Values.NsfwAllowance.ALLOWED.name,
Values.NsfwAllowance.PARTIAL.name,
Values.NsfwAllowance.BLOCKED.name
)
defaultValue = Values.NsfwAllowance.ALLOWED.name
summary = "%s"
}
preferenceCategory {
infoPreference(R.string.parental_controls_info)
}
}
}
@@ -78,9 +78,9 @@ class SettingsReaderController : SettingsController() {
intListPreference { intListPreference {
key = Keys.readerTheme key = Keys.readerTheme
titleRes = R.string.pref_reader_theme titleRes = R.string.pref_reader_theme
entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background) entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background, R.string.smart_based_on_page, R.string.smart_based_on_page_and_theme)
entryValues = arrayOf("1", "2", "0") entryValues = arrayOf("1", "2", "0", "3", "4")
defaultValue = "1" defaultValue = "3"
summary = "%s" summary = "%s"
} }
switchPreference { switchPreference {
@@ -294,6 +294,11 @@ class SettingsReaderController : SettingsController() {
titleRes = R.string.pref_crop_borders titleRes = R.string.pref_crop_borders
defaultValue = false defaultValue = false
} }
switchPreference {
key = Keys.webtoonEnableZoomOut
titleRes = R.string.enable_zoom_out
defaultValue = false
}
} }
preferenceCategory { preferenceCategory {
@@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog
import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog
import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.infoPreference import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.initThenAdd import eu.kanade.tachiyomi.util.preference.initThenAdd
@@ -20,8 +22,6 @@ import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.LoginPreference
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
import eu.kanade.tachiyomi.widget.preference.TrackLogoutDialog
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SettingsTrackingController : class SettingsTrackingController :
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.widget.preference package eu.kanade.tachiyomi.ui.setting.track
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference
import kotlinx.android.synthetic.main.pref_account_login.view.login import kotlinx.android.synthetic.main.pref_account_login.view.login
import kotlinx.android.synthetic.main.pref_account_login.view.password import kotlinx.android.synthetic.main.pref_account_login.view.password
import kotlinx.android.synthetic.main.pref_account_login.view.username import kotlinx.android.synthetic.main.pref_account_login.view.username
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.widget.preference package eu.kanade.tachiyomi.ui.setting.track
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
@@ -9,19 +9,18 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.webkit.WebViewClientCompat
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
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 eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@@ -100,8 +99,8 @@ class WebViewActivity : BaseActivity<WebviewActivityBinding>() {
} }
binding.webview.webViewClient = object : WebViewClientCompat() { binding.webview.webViewClient = object : WebViewClientCompat() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
view.loadUrl(request.url.toString()) view.loadUrl(url)
return true return true
} }
@@ -34,14 +34,12 @@ object DiskUtil {
* Gets the available space for the disk that a file path points to, in bytes. * Gets the available space for the disk that a file path points to, in bytes.
*/ */
fun getAvailableStorageSpace(f: UniFile): Long { fun getAvailableStorageSpace(f: UniFile): Long {
val stat = try { return try {
StatFs(f.filePath) val stat = StatFs(f.uri.path)
stat.availableBlocksLong * stat.blockSizeLong
} catch (_: Exception) { } catch (_: Exception) {
// Assume that exception is thrown when path is on external storage -1L
StatFs(Environment.getExternalStorageDirectory().path)
} }
return stat.availableBlocksLong * stat.blockSizeLong
} }
/** /**
@@ -1,7 +1,15 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import eu.kanade.tachiyomi.R
import java.io.InputStream import java.io.InputStream
import java.net.URLConnection import java.net.URLConnection
import kotlin.math.abs
object ImageUtil { object ImageUtil {
@@ -71,4 +79,205 @@ object ImageUtil {
GIF("image/gif", "gif"), GIF("image/gif", "gif"),
WEBP("image/webp", "webp") WEBP("image/webp", "webp")
} }
// SY -->
fun autoSetBackground(image: Bitmap?, alwaysUseWhite: Boolean, context: Context): Drawable {
val backgroundColor = if (alwaysUseWhite) Color.WHITE else {
context.getResourceColor(R.attr.colorPrimary)
}
if (image == null) return ColorDrawable(backgroundColor)
if (image.width < 50 || image.height < 50) {
return ColorDrawable(backgroundColor)
}
val top = 5
val bot = image.height - 5
val left = (image.width * 0.0275).toInt()
val right = image.width - left
val midX = image.width / 2
val midY = image.height / 2
val offsetX = (image.width * 0.01).toInt()
val offsetY = (image.height * 0.01).toInt()
val topLeftIsDark = isDark(image.getPixel(left, top))
val topRightIsDark = isDark(image.getPixel(right, top))
val midLeftIsDark = isDark(image.getPixel(left, midY))
val midRightIsDark = isDark(image.getPixel(right, midY))
val topMidIsDark = isDark(image.getPixel(midX, top))
val botLeftIsDark = isDark(image.getPixel(left, bot))
val botRightIsDark = isDark(image.getPixel(right, bot))
var darkBG = (topLeftIsDark && (botLeftIsDark || botRightIsDark || topRightIsDark || midLeftIsDark || topMidIsDark)) ||
(topRightIsDark && (botRightIsDark || botLeftIsDark || midRightIsDark || topMidIsDark))
if (!isWhite(image.getPixel(left, top)) && pixelIsClose(image.getPixel(left, top), image.getPixel(midX, top)) &&
!isWhite(image.getPixel(midX, top)) && pixelIsClose(image.getPixel(midX, top), image.getPixel(right, top)) &&
!isWhite(image.getPixel(right, top)) && pixelIsClose(image.getPixel(right, top), image.getPixel(right, bot)) &&
!isWhite(image.getPixel(right, bot)) && pixelIsClose(image.getPixel(right, bot), image.getPixel(midX, bot)) &&
!isWhite(image.getPixel(midX, bot)) && pixelIsClose(image.getPixel(midX, bot), image.getPixel(left, bot)) &&
!isWhite(image.getPixel(left, bot)) && pixelIsClose(image.getPixel(left, bot), image.getPixel(left, top))
) {
return ColorDrawable(image.getPixel(left, top))
}
if (isWhite(image.getPixel(left, top)).toInt() +
isWhite(image.getPixel(right, top)).toInt() +
isWhite(image.getPixel(left, bot)).toInt() +
isWhite(image.getPixel(right, bot)).toInt() > 2
) {
darkBG = false
}
var blackPixel = when {
topLeftIsDark -> image.getPixel(left, top)
topRightIsDark -> image.getPixel(right, top)
botLeftIsDark -> image.getPixel(left, bot)
botRightIsDark -> image.getPixel(right, bot)
else -> backgroundColor
}
var overallWhitePixels = 0
var overallBlackPixels = 0
var topBlackStreak = 0
var topWhiteStreak = 0
var botBlackStreak = 0
var botWhiteStreak = 0
outer@ for (x in intArrayOf(left, right, left - offsetX, right + offsetX)) {
var whitePixelsStreak = 0
var whitePixels = 0
var blackPixelsStreak = 0
var blackPixels = 0
var blackStreak = false
var whiteStrak = false
val notOffset = x == left || x == right
for ((index, y) in (0 until image.height step image.height / 25).withIndex()) {
val pixel = image.getPixel(x, y)
val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y)
if (isWhite(pixel)) {
whitePixelsStreak++
whitePixels++
if (notOffset) {
overallWhitePixels++
}
if (whitePixelsStreak > 14) {
whiteStrak = true
}
if (whitePixelsStreak > 6 && whitePixelsStreak >= index - 1) {
topWhiteStreak = whitePixelsStreak
}
} else {
whitePixelsStreak = 0
if (isDark(pixel) && isDark(pixelOff)) {
blackPixels++
if (notOffset) {
overallBlackPixels++
}
blackPixelsStreak++
if (blackPixelsStreak >= 14) {
blackStreak = true
}
continue
}
}
if (blackPixelsStreak > 6 && blackPixelsStreak >= index - 1) {
topBlackStreak = blackPixelsStreak
}
blackPixelsStreak = 0
}
if (blackPixelsStreak > 6) {
botBlackStreak = blackPixelsStreak
} else if (whitePixelsStreak > 6) {
botWhiteStreak = whitePixelsStreak
}
when {
blackPixels > 22 -> {
if (x == right || x == right + offsetX) {
blackPixel = when {
topRightIsDark -> image.getPixel(right, top)
botRightIsDark -> image.getPixel(right, bot)
else -> blackPixel
}
}
darkBG = true
overallWhitePixels = 0
break@outer
}
blackStreak -> {
darkBG = true
if (x == right || x == right + offsetX) {
blackPixel = when {
topRightIsDark -> image.getPixel(right, top)
botRightIsDark -> image.getPixel(right, bot)
else -> blackPixel
}
}
if (blackPixels > 18) {
overallWhitePixels = 0
break@outer
}
}
whiteStrak || whitePixels > 22 -> darkBG = false
}
}
val topIsBlackStreak = topBlackStreak > topWhiteStreak
val bottomIsBlackStreak = botBlackStreak > botWhiteStreak
if (overallWhitePixels > 9 && overallWhitePixels > overallBlackPixels) {
darkBG = false
}
if (topIsBlackStreak && bottomIsBlackStreak) {
darkBG = true
}
if (darkBG) {
return if (isWhite(image.getPixel(left, bot)) && isWhite(image.getPixel(right, bot))) {
GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor)
)
} else if (isWhite(image.getPixel(left, top)) && isWhite(image.getPixel(right, top))) {
GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)
)
} else ColorDrawable(blackPixel)
}
if (topIsBlackStreak || (
topLeftIsDark && topRightIsDark &&
isDark(image.getPixel(left - offsetX, top)) && isDark(image.getPixel(right + offsetX, top)) &&
(topMidIsDark || overallBlackPixels > 9)
)
) {
return GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(blackPixel, blackPixel, backgroundColor, backgroundColor)
)
} else if (bottomIsBlackStreak || (
botLeftIsDark && botRightIsDark &&
isDark(image.getPixel(left - offsetX, bot)) && isDark(image.getPixel(right + offsetX, bot)) &&
(isDark(image.getPixel(midX, bot)) || overallBlackPixels > 9)
)
) {
return GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
intArrayOf(backgroundColor, backgroundColor, blackPixel, blackPixel)
)
}
return ColorDrawable(backgroundColor)
}
private fun isDark(color: Int): Boolean {
return Color.red(color) < 40 && Color.blue(color) < 40 && Color.green(color) < 40 &&
Color.alpha(color) > 200
}
private fun isWhite(color: Int): Boolean {
return Color.red(color) + Color.blue(color) + Color.green(color) > 740
}
private fun Boolean.toInt() = if (this) 1 else 0
private fun pixelIsClose(color1: Int, color2: Int): Boolean {
return abs(Color.red(color1) - Color.red(color2)) < 30 &&
abs(Color.green(color1) - Color.green(color2)) < 30 &&
abs(Color.blue(color1) - Color.blue(color2)) < 30
}
// SY <--
} }
@@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.util.system
import android.annotation.TargetApi
import android.os.Build
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
@Suppress("OverridingDeprecatedMember")
abstract class WebViewClientCompat : WebViewClient() {
open fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
return false
}
open fun shouldInterceptRequestCompat(view: WebView, url: String): WebResourceResponse? {
return null
}
open fun onReceivedErrorCompat(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String,
isMainFrame: Boolean
) {
}
@TargetApi(Build.VERSION_CODES.N)
final override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return shouldOverrideUrlCompat(view, request.url.toString())
}
final override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
return shouldOverrideUrlCompat(view, url)
}
final override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return shouldInterceptRequestCompat(view, request.url.toString())
}
final override fun shouldInterceptRequest(
view: WebView,
url: String
): WebResourceResponse? {
return shouldInterceptRequestCompat(view, url)
}
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError
) {
onReceivedErrorCompat(
view, error.errorCode, error.description?.toString(),
request.url.toString(), request.isForMainFrame
)
}
final override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String
) {
onReceivedErrorCompat(view, errorCode, description, failingUrl, failingUrl == view.url)
}
@TargetApi(Build.VERSION_CODES.M)
final override fun onReceivedHttpError(
view: WebView,
request: WebResourceRequest,
error: WebResourceResponse
) {
onReceivedErrorCompat(
view, error.statusCode, error.reasonPhrase,
request.url
.toString(),
request.isForMainFrame
)
}
}
@@ -18,8 +18,9 @@ class ElevationAppBarLayout @JvmOverloads constructor(
origStateAnimator = stateListAnimator origStateAnimator = stateListAnimator
} }
fun enableElevation() { fun enableElevation(liftOnScroll: Boolean) {
stateListAnimator = origStateAnimator stateListAnimator = origStateAnimator
isLiftOnScroll = liftOnScroll
} }
fun disableElevation() { fun disableElevation() {
@@ -109,6 +109,22 @@ open class ExtendedNavigationView @JvmOverloads constructor(
} }
// SY --> // SY -->
class DrawableSelection(val id: Int, group: Group, stringResId: Int, val drawable: Int) : MultiStateGroup(stringResId, group) {
companion object {
const val NOT_SELECTED = 0
const val SELECTED = 1
}
override fun getStateDrawable(context: Context): Drawable? {
return when (state) {
SELECTED -> tintVector(context, drawable, R.attr.colorAccent)
NOT_SELECTED -> tintVector(context, drawable, R.attr.colorOnSurface)
else -> null
}
}
}
class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) { class TriStateGroup(resId: Int, group: Group) : MultiStateGroup(resId, group) {
companion object { companion object {
@@ -17,10 +17,7 @@ class ThemedSwipeRefreshLayout @JvmOverloads constructor(context: Context, attrs
// Background is controlled with "swipeRefreshLayoutProgressSpinnerBackgroundColor" in XML // Background is controlled with "swipeRefreshLayoutProgressSpinnerBackgroundColor" in XML
// This updates the progress arrow color // This updates the progress arrow color
setColorSchemeColors( val white = ContextCompat.getColor(context, R.color.md_white_1000)
ContextCompat.getColor(context, R.color.md_white_1000), setColorSchemeColors(white, white, white)
ContextCompat.getColor(context, R.color.md_white_1000),
ContextCompat.getColor(context, R.color.md_white_1000)
)
} }
} }

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