Compare commits

..

264 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
Jobobby04 b9583a31c9 Update readme
Remote Dispatch Action Initiator / ping-pong (push) Failing after 10s
2020-08-02 16:29:13 -04:00
Jobobby04 926fa85ccd Release 1.1.1 2020-08-02 16:26:07 -04:00
Jobobby04 b91252df67 HBrowse url matching sorta fixed 2020-08-02 16:24:54 -04:00
arkon 3893c90eb2 Make download badges lighter to improve contrast (closes #3571)
(cherry picked from commit 2e9d89574d)
2020-08-02 15:11:29 -04:00
Jozef Hollý d5f4783aca Translated using Weblate (Russian) (#3549)
Currently translated at 100.0% (565 of 565 strings)

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

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 (Indonesian)

Currently translated at 100.0% (565 of 565 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (565 of 565 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (565 of 565 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Georgian)

Currently translated at 9.3% (53 of 564 strings)

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

Translated using Weblate (Tagalog)

Currently translated at 73.9% (417 of 564 strings)

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

Translated using Weblate (Serbian)

Currently translated at 79.9% (451 of 564 strings)

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

Translated using Weblate (Thai)

Currently translated at 58.1% (328 of 564 strings)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 88.6% (500 of 564 strings)

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

Translated using Weblate (Czech)

Currently translated at 64.0% (361 of 564 strings)

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

Translated using Weblate (Korean)

Currently translated at 57.9% (327 of 564 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 35.6% (201 of 564 strings)

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

Translated using Weblate (Bengali)

Currently translated at 60.4% (341 of 564 strings)

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

Translated using Weblate (Czech)

Currently translated at 63.8% (360 of 564 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
(cherry picked from commit 569c99496b)
2020-08-02 15:11:12 -04:00
arkon b0bcfa9db0 Fix crash when filter groups contain items with identical names (closes #3568)
(cherry picked from commit ea3b8767de)
2020-08-02 15:11:01 -04:00
arkon 01ea86ab90 Move download warnings/errors to separate notification channel
(cherry picked from commit 8e8c30c1eb)
2020-08-02 15:10:52 -04:00
arkon 475299d9b3 Revert "Downgrade coroutines and flow-preferences"
This reverts commit b47ee8857b.

(cherry picked from commit d921ba81c8)
2020-08-02 15:10:43 -04:00
arkon 951bb1f3c6 Fix downloads not working for custom SD card paths (closes #3564)
(cherry picked from commit ad9f646102)
2020-08-02 15:10:34 -04:00
arkon 1f7e69e13c Don't show completed notification if download error notification was shown
(cherry picked from commit 2ef277bcef)
2020-08-02 15:10:23 -04:00
arkon 5fbaa7d6be Fix history item icon tint in light blue theme
(cherry picked from commit 9e396e1624)
2020-08-02 15:09:22 -04:00
Jobobby04 cce1b135c9 Tweak HBrowse migration 2020-08-02 15:08:47 -04:00
Jobobby04 b344a3944e Fix certain HBrowse manga 2020-08-02 15:06:49 -04:00
arkon 7f416bda7c Fix dividers in migrate list
(cherry picked from commit 9708d84e60)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/manga/MigrationMangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesController.kt
2020-08-02 00:58:13 -04:00
arkon 3b08c7fdea Fix last used source pinned status
(cherry picked from commit 4efc195548)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt
2020-08-02 00:55:28 -04:00
Jobobby04 e346d95b0e Delegate HBrowse 2020-08-02 00:50:52 -04:00
arkon 0fe8990f99 Filter out chapter entries with duplicate URLs (fixes #3552)
(cherry picked from commit 0d15cbe334)
2020-08-01 16:40:43 -04:00
jobobby04 35ed8e2d34 Update README.md 2020-08-01 15:29:14 -04:00
Jobobby04 12d01b9da3 Release 1.1.0
Remote Dispatch Action Initiator / ping-pong (push) Failing after 10s
2020-08-01 15:07:32 -04:00
Jobobby04 2b7ffc8ba2 Fix 8Muses chapters 2020-08-01 14:34:28 -04:00
arkon 1b91062767 Fix for reader crash in < Android 9
(cherry picked from commit 85ed7a7457)
2020-08-01 14:03:26 -04:00
Jobobby04 4a71eb2ff0 Fix hitomi.la thumbnails 2020-08-01 13:17:16 -04:00
arkon 6fe9284c07 Update issue templates
(cherry picked from commit b8a98ef5e4)

# Conflicts:
#	.github/readme-images/screens.png
#	app/build.gradle
2020-08-01 13:01:00 -04:00
Jozef Hollý 51c2a1b048 Translated using Weblate (French) (#3528)
Currently translated at 99.8% (563 of 564 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.7% (557 of 564 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Marathi)

Currently translated at 41.4% (234 of 564 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.8% (552 of 564 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (564 of 564 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Malay)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Filipino)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Czech)

Currently translated at 63.9% (360 of 563 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
(cherry picked from commit c8fa90f473)
2020-08-01 12:58:31 -04:00
arkon 03b3046ece Downgrade coroutines and flow-preferences
(cherry picked from commit b47ee8857b)

# Conflicts:
#	app/build.gradle
2020-08-01 12:57:52 -04:00
arkon ea2f050f86 Temporarily revert to stable version of androidx.biometric (closes #3425)
(cherry picked from commit 131dfa62c4)
2020-08-01 12:54:04 -04:00
arkon f41077449a Temporarily unrevert crop borders unification (closes #3487)
Reverts 1920568057

(cherry picked from commit 6a5af438dd)
2020-08-01 12:53:54 -04:00
arkon aad0ac7296 Shift WebView checks to necessary places only to allow for basic usage
(cherry picked from commit ccc0a61158)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/ForceCloseActivity.kt
2020-08-01 12:53:39 -04:00
arkon 5e59d05598 Fix tap region for manga summary
(cherry picked from commit e990ad25eb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
#	app/src/main/res/layout/manga_info_header.xml
2020-08-01 12:51:55 -04:00
arkon 337d270d2a Actually fix library search properly
(cherry picked from commit 98a4d1e763)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2020-08-01 12:34:35 -04:00
arkon 09c9e15281 Fix library search query being lost when returning (closes #3473)
(cherry picked from commit f762598c5c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
2020-08-01 12:32:21 -04:00
arkon 057ccf74ce More core-ktx usages
(cherry picked from commit ec56c27071)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterDividerItemDecoration.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2020-08-01 12:26:35 -04:00
arkon c21771823c Use Kotlin extensions for preference editing
(cherry picked from commit eb0e0a1952)
2020-08-01 12:17:18 -04:00
arkon 987e5bcf33 Make source options dialog into a controller to retain state
(cherry picked from commit 01a837fde6)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
2020-08-01 12:17:03 -04:00
arkon 3920a5a73b Hide cutout option when appropriate in reader settings sheet (closes #2982)
(cherry picked from commit b9488645d4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
2020-08-01 12:10:44 -04:00
arkon 00701aeda1 Update ConstraintLayout
(cherry picked from commit 7a94b477cb)
2020-08-01 12:09:31 -04:00
arkon bb47188d5c Fix download status updates not appearing in chapters list (fixes #3358)
(cherry picked from commit 99710b45d1)
2020-08-01 12:09:21 -04:00
Jobobby04 06e57b790e Blacklist the nHentai extension non-english sources, and a few debug function edits 2020-07-30 16:15:38 -04:00
Jobobby04 ff48e89161 Now really fix internal sources download badges not showing up, plus some refactoring and testing debug functions 2020-07-30 15:56:18 -04:00
arkon e338bb0f47 Split download notifications into progress and complete channels
(cherry picked from commit 3813743e3d)
2020-07-29 23:21:01 -04:00
Jobobby04 ae48c1d7d4 Fix nHentai and E-Hentai manga download badges when the extension is installed as well. Closes #55 2020-07-29 23:13:58 -04:00
Jobobby04 243c65d012 Cleanup unused paramater 2020-07-29 21:13:45 -04:00
Jobobby04 e9903a6678 Pressing download unread chapters on E/ExHentai manga in your library will only download the latest version of the gallery 2020-07-29 20:53:07 -04:00
Jobobby04 afe32f1099 Pressing download next on a E/Exhentai manga will download the latest chapter 2020-07-29 20:41:19 -04:00
Jobobby04 acf2ad7c77 E/ExHentai manga fab, dont return chapter if its already read 2020-07-29 20:31:25 -04:00
Jobobby04 4286fd606a Update realms to 7.0.1 in hope of fixing android 7.1.2 SY 2020-07-29 20:23:30 -04:00
Jimmy Low 70d134b375 [Feature Request] - Download Complete Remidner #3475 (#3527)
* [Feat] Show a download complete notification channel when all downloads are completed. Auto cancels when onclick and navigate to download screen.

* [Feat] Update the download message string to shorten the length.

(cherry picked from commit 7e73ede47a)
2020-07-29 19:27:19 -04:00
arkon a8d0564eb0 Replace VectorDrawableCompat.create() with AppCompatResources.getDrawable()
Fixes crash when loading pin icon in Android 5/6.

(cherry picked from commit 9bb2334b69)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/widget/ExtendedNavigationView.kt
2020-07-29 18:43:43 -04:00
Jobobby04 df6bbbd4c6 Fix source category deletion crash 2020-07-29 00:26:14 -04:00
Jobobby04 7a08fa3398 Hide the pin button in merge with another 2020-07-29 00:02:18 -04:00
Jobobby04 46d9c024da Recommendations crash fix 2020-07-28 23:38:36 -04:00
Jobobby04 2c49466a42 Fix crash on E-Hentai when pressing the FAB when the chapters havent loaded yet 2020-07-28 22:33:43 -04:00
Jobobby04 a6cba5c87d Add special view for browsing E/Exhentai! All the important info is now in front of your face when browsing, it is on by default and can be toggled off in the E-Hentai settings. Let me know if you find any errors 2020-07-28 16:55:33 -04:00
arkon 032504f128 Fix getting stuck in chapter loop when chapters have identical URLs
(cherry picked from commit b0106aa420)
2020-07-27 17:48:08 -04:00
Jobobby04 f5b6fc5b54 Cleanup 2020-07-27 13:33:42 -04:00
Jobobby04 c0e1ca1185 Parse more info when browsing E/Exhentai (cont) 2020-07-26 21:45:44 -04:00
arkon fa812830b8 Explicitly destroy webview on activity destroy
(cherry picked from commit 33e5fea96c)
2020-07-26 18:15:14 -04:00
arkon b4c68f454d Prevent spamming updates with newly favorited manga
(cherry picked from commit f0a1dcd120)
2020-07-26 18:15:06 -04:00
arkon a13166b69d Fix source item flashing when pinning
(cherry picked from commit 26d5a87bef)
2020-07-26 18:14:41 -04:00
arkon 0556c5c2ff Show lang code in source long press dialog
(cherry picked from commit 52ae208df3)
2020-07-26 18:14:32 -04:00
arkon c449a59696 Remove explicit source browse button, tint pin icon when pinned
(cherry picked from commit 34aaa7fb0a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt
2020-07-26 18:14:05 -04:00
arkon 2339388d6f Fix Chinese plurals
(cherry picked from commit a8c784355c)
2020-07-26 18:01:26 -04:00
arkon 9c669d040a Don't show chapter number in history item when unknown
(cherry picked from commit 0aed93becf)
2020-07-26 18:01:16 -04:00
Jozef Hollý 82acb4412a Translated using Weblate (Croatian) (#3421)
Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.5% (555 of 563 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Latvian)

Currently translated at 34.6% (195 of 563 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Latvian)

Currently translated at 27.4% (154 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 26.6% (150 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 25.2% (142 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 21.8% (123 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Romanian)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (553 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (553 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 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% (562 of 562 strings)

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

Translated using Weblate (Georgian)

Currently translated at 6.9% (39 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 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% (562 of 562 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% (562 of 562 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% (562 of 562 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 97.8% (550 of 562 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Vietnamese)

Currently translated at 87.5% (492 of 562 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Polish)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 56.0% (315 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.0% (506 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 99.6% (560 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 99.2% (558 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (560 of 560 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (560 of 560 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Italian)

Currently translated at 99.6% (557 of 559 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Swedish)

Currently translated at 99.4% (553 of 556 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Korean)

Currently translated at 58.6% (326 of 556 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 36.6% (204 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Bengali)

Currently translated at 61.8% (344 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 86.8% (483 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 77.3% (430 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 75.7% (421 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 67.9% (378 of 556 strings)

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

Translated using Weblate (Japanese)

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 67.8% (377 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 65.6% (365 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 64.5% (359 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 59.7% (332 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (545 of 556 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.4% (542 of 556 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (541 of 556 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 17.4% (97 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Hindi)

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.6% (554 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (556 of 556 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Czech)

Currently translated at 65.2% (362 of 555 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
(cherry picked from commit 1ea0804209)
2020-07-26 18:01:04 -04:00
Jobobby04 23b0c3305d Parse more info when browsing E/ExHentai 2020-07-26 17:29:16 -04:00
Jobobby04 47373a9483 Fix manga info divider for the first chapter in certain situations 2020-07-25 23:15:51 -04:00
arkon 87e3a610e1 Add pin icon to sources list (closes #2862)
(cherry picked from commit a52fbb012a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt
2020-07-25 22:40:43 -04:00
arkon 94d14af2a4 Add operator functions for handling set preferences
(cherry picked from commit 2dc47352f8)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt
2020-07-25 22:30:21 -04:00
arkon 99becd4fd6 Show message when searching with no pinned sources
(cherry picked from commit e95a5be21d)
2020-07-25 22:19:08 -04:00
arkon f21ef47c87 Fix weird backstack behaviour after clearing database
Shouldn't affect anything since controllers are recreated when entering different sections.

(cherry picked from commit abd69d4f91)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2020-07-25 22:18:59 -04:00
arkon 2ef7212128 Minor optimizations for local source dir lookups
(cherry picked from commit d749e309f8)
2020-07-25 22:13:13 -04:00
arkon 0003d11da3 Update dependencies
(cherry picked from commit e4d075fb91)
2020-07-25 22:13:04 -04:00
arkon bf49023693 Lazily find chapter directories
(cherry picked from commit 71c6c71081)
2020-07-25 22:12:53 -04:00
arkon e1bdb1dd0f Inline extension functions
(cherry picked from commit d2b14bcfc4)
2020-07-25 22:12:44 -04:00
arkon 2222c030b8 Increase dismiss timeout for what's new snackbar
(cherry picked from commit 2c04c81bd1)
2020-07-25 22:12:35 -04:00
arkon 1631bfd5c6 Use some more core-ktx extensions
(cherry picked from commit dd66c83c50)
2020-07-25 22:11:55 -04:00
arkon 72f3ebb70d Replace custom visibility extension functions
(cherry picked from commit 9e51d82154)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SourceHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2020-07-25 22:11:44 -04:00
arkon 17e5ebd171 Hide manga title in toolbar when at top
(cherry picked from commit bdc441a5be)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2020-07-25 21:29:33 -04:00
arkon b2059288b7 Update Material Components
(cherry picked from commit 2dcb73700b)
2020-07-25 21:20:55 -04:00
arkon f24926fc81 More consistent library list view padding (closes #3509)
(cherry picked from commit 9a55cf880e)
2020-07-25 21:20:39 -04:00
arkon aba324a461 Hide tracking button if none logged in, show for non-favorited manga (closes #3507)
(cherry picked from commit 6742cdeb8b)
2020-07-25 21:20:21 -04:00
Jobobby04 4a19f8cff2 Remove the dividers between the new view 2020-07-25 21:20:07 -04:00
arkon 302db11482 Remove divider between manga info header and chapters header
(cherry picked from commit c37377bffa)
2020-07-25 21:05:26 -04:00
Ken Swenson 1af2698b72 fix: Download on WiFi regardless of metered status (#3489)
* fix: Download on WiFi regardless of metered status

fixes #3395

* fix: check if not WiFi rather than checking if connection is mobile

(cherry picked from commit 76147a9be7)
2020-07-25 21:05:16 -04:00
Jobobby04 3e9c8dbfd2 Add a special view to replace descriptions for integrated and delegated sources!
As the integrated and delegated websites don't actually have descriptions, just info, I decided to make a special view for them! with all the info you need available to you in front of your face, there is now no need to go searching through the description! This is likely the most work I have put into 1 feature in the whole time I have been developing TachiyomiSY!
2020-07-25 21:04:13 -04:00
Jobobby04 a38cb2ab5f Downloader conflict fixing 2020-07-24 23:19:57 -04:00
Jobobby04 589464d723 More rename downloaded chapter tweaks 2020-07-24 23:11:36 -04:00
Jobobby04 d7f3b399f4 Make the rename function do less lookups 2020-07-24 22:41:07 -04:00
Jobobby04 646aeb66c5 Inline the foreach functions 2020-07-24 22:37:54 -04:00
Jobobby04 135f0bdd95 Add scanlator to download pending deleter chapter data class 2020-07-24 22:26:31 -04:00
Jobobby04 80394dab4a Tweaks based on comments in the PR 2020-07-24 22:16:23 -04:00
Jobobby04 75e9911317 SY maybe supports J2k downloads now 2020-07-24 21:46:59 -04:00
Jobobby04 a311a3b497 Fix auto captcha opening when you dont have the option enabled (temp fix) 2020-07-24 12:36:20 -04:00
Jobobby04 f3b6855684 Add cancel buttons to tag watching and tag filtering settings. Fix the - sign saying there was a input error in the tag filtering input 2020-07-20 20:54:39 -04:00
Jobobby04 2ee69c2ac4 Fixes for a few strings 2020-07-20 15:15:28 -04:00
Jobobby04 ff0516726b pt-rBR fixes and updates by SamOak! 2020-07-19 22:51:29 -04:00
Jobobby04 7e5de79d5f Revert "Migrate library to ViewPager2"
This reverts commit 570db67894.
2020-07-19 21:56:41 -04:00
arkon dabb7a0494 Don't initialize mangas if viewing source in list view if on metered connection
(cherry picked from commit c401915fb5)
2020-07-19 21:56:10 -04:00
Jobobby04 ff1e0d7578 Allow hitomi users to select whether they want to download images as Webp or not 2020-07-19 20:06:47 -04:00
Jobobby04 4771fa529d Update latest tab with the new global search features 2020-07-19 19:52:50 -04:00
Jobobby04 8e94afb9c1 Add a option to put the recommendations into the overflow menu instead of the manga page 2020-07-19 19:22:08 -04:00
arkon 570db67894 Migrate library to ViewPager2
(cherry picked from commit 2a202bd510)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/widget/RecyclerViewPagerAdapter.kt
2020-07-19 18:56:55 -04:00
arkon 875b2fbccd Rename chapters_controller to manga_controller
(cherry picked from commit dcd8ed08fc)
2020-07-19 18:52:54 -04:00
arkon e562f0392d Explicitly show "No results found" in global search instead of hiding row
(cherry picked from commit d3ebedeef2)
2020-07-19 18:52:44 -04:00
arkon e142af00fa Add ripple to global search source title
(cherry picked from commit d2e2ebbe45)
2020-07-19 18:52:35 -04:00
arkon a5c4098109 Show tracker status in button
(cherry picked from commit a443dc3040)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
#	app/src/main/res/drawable/ic_done_white_18dp.xml
2020-07-19 18:52:23 -04:00
arkon 62091790a5 Update subsampling-scale-image-view
(cherry picked from commit 4e6cc013e5)

# Conflicts:
#	build.gradle.kts
2020-07-19 18:48:25 -04:00
arkon 7ddfedd9c7 Switch to tachiyomiorg fork of subsampling-scale-image-view
(cherry picked from commit 0c65d54d89)
2020-07-19 18:44:22 -04:00
arkon 70a779e4d0 Manga about section layout tweaks
(cherry picked from commit ccd0e0cdfe)

# Conflicts:
#	app/src/main/res/layout/manga_info_header.xml
2020-07-19 18:44:16 -04:00
arkon fd40f35371 Move chapter filter/sort/display settings into a sheet
(cherry picked from commit 9278ca3f5e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/res/menu/chapters.xml
2020-07-19 18:39:59 -04:00
arkon f66aff9ed7 Toggle about section when tapping on header/empty space
(cherry picked from commit d7a70b962b)
2020-07-19 18:33:23 -04:00
arkon 29ad0e091f Long press favorite button to manage categories
(cherry picked from commit fff0f841fa)
2020-07-19 18:33:13 -04:00
arkon fa580aa3c9 Fix checked state for manga header buttons
(cherry picked from commit 8ba426350f)
2020-07-19 18:32:57 -04:00
arkon bd8bc3a3cb Remove redundant Reading Mode header
(cherry picked from commit 5452e29840)
2020-07-19 18:32:39 -04:00
arkon 52f2644035 Tweak track search dialog list item paddings
(cherry picked from commit 148f8e6d11)
2020-07-19 18:32:26 -04:00
arkon 7a97d6f20d Update Android Gradle plugin for Android Studio 4.0.1
(cherry picked from commit 13a5662a84)
2020-07-19 18:25:21 -04:00
arkon 8de67c49bc Include source ID if name not found in restore error log (closes #3018)
(cherry picked from commit 6713a7ae3c)
2020-07-19 18:25:11 -04:00
arkon 7530a7bd4e Move edit categories to overflow
(cherry picked from commit 88ee86b7ef)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/res/menu/chapters.xml
2020-07-19 18:24:55 -04:00
arkon 1ac7043163 Allow category names with different casing (fixes #3465)
(cherry picked from commit 4bc2288806)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/models/Category.kt
2020-07-19 18:20:05 -04:00
Jobobby04 8e24797e50 More translation fixes for pt-rBR by SamOak! 2020-07-19 18:15:41 -04:00
Jobobby04 4513af8425 Make the resume/start button go to the latest version for EH manga 2020-07-19 18:15:38 -04:00
Jobobby04 78d1a6cecb Cleanup library a bit 2020-07-19 14:23:35 -04:00
Jobobby04 a9e9fe59c6 Updates to the pt-rBR translation from SamOak! 2020-07-17 14:03:03 -04:00
Jobobby04 a8f2f03562 Tsumino is now supported by the new tag display, you may have to refresh your Tsumino manga for them to work properly. Also Set the groundwork for more special features 2020-07-16 22:37:33 -04:00
Jobobby04 9e986bbeb6 Add pt-rBR translation, thanks to SamOak! 2020-07-16 17:29:05 -04:00
Jobobby04 b904bf99e8 Cleanup 2020-07-16 17:27:47 -04:00
Jobobby04 8b95d93a96 Add custom tag view for namespaced sources (E-Hentai, nHentai, Hitomi.la, and Pururin) 2020-07-16 17:27:36 -04:00
Jobobby04 74012e0830 Revert tweaked browse tab view 2020-07-15 19:16:51 -04:00
Jobobby04 362f0a6671 Made almost all the strings SY uses translatable! If people would like to help translate, feel free to join the Tachiyomi discord server (https://discord.gg/tachiyomi), and jump in the tachiyomi-az-sy channel and I can give you a rundown on how to do it 2020-07-15 19:16:21 -04:00
Jobobby04 0ca87a3763 Use androidx preferenceManager for EHLogLevel 2020-07-14 01:50:17 -04:00
Jobobby04 840ab68922 Global search and latest card fixes 2020-07-14 01:49:23 -04:00
Jobobby04 4663d64c05 Cleanup some errors 2020-07-14 01:01:55 -04:00
Jobobby04 4b7c33be16 Remove unused build.gradle option 2020-07-14 00:04:13 -04:00
Jobobby04 8c40e4d635 Try to fix my firebase issues with dev builds 2020-07-13 13:48:53 -04:00
arkon eaae98d072 Enable more WebView settings to better mimic regular browser
(cherry picked from commit a928d9fa0b)
2020-07-13 13:38:50 -04:00
arkon 5ffc21fc9e Don't capitalize buttons (closes #3454)
(cherry picked from commit d8f4e6b45f)
2020-07-13 13:38:40 -04:00
Jobobby04 294caa25a4 Manga cover editing fixes 2020-07-13 13:25:05 -04:00
Jobobby04 923f5213cd Add author and artist wrapping if a EH based source 2020-07-12 23:27:35 -04:00
Jobobby04 8434b880c6 Fix date added not showing up 2020-07-12 22:50:08 -04:00
Jobobby04 badd43046b Reimplement Eh tag searching into the new manga page 2020-07-12 22:50:05 -04:00
arkon 00d5fd8fe4 Replace some usages of findViewById
(cherry picked from commit 5ef5087406)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2020-07-12 19:37:18 -04:00
arkon 3bd6b8524f Fix manga info actions being cut off
(cherry picked from commit 135c371d88)

# Conflicts:
#	app/src/main/res/layout/manga_info_header.xml
2020-07-12 19:34:00 -04:00
Jobobby04 362ba1bf69 Cleanup unused library migration code 2020-07-12 19:32:08 -04:00
arkon 450b76f495 Remove unused CoverCache param from LibraryController
(cherry picked from commit 966c196f4a)
2020-07-12 19:30:01 -04:00
arkon 1188ee10d8 Use view binding for sheets
(cherry picked from commit dc43e41896)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
2020-07-12 19:29:50 -04:00
Jobobby04 372e570fac Use Tachi previews info + chapters manga page, plus of course SY features integrated into it
Add missed invert tap settings
Add missed extension open in settings overflow menu option
Cleanup
2020-07-12 19:21:29 -04:00
arkon 8ab2a823b5 Speed up controller fade and tab expansion animations
(cherry picked from commit 4809d06d04)
2020-07-12 16:39:58 -04:00
arkon 7fb197a752 Move edit cover to manga info
(cherry picked from commit 9f7fda0bc5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
#	app/src/main/res/menu/chapters.xml
2020-07-12 16:39:52 -04:00
arkon 7046d304e0 Hide toolbars when reader color filter sheet is opened
(cherry picked from commit 66ef1a8206)
2020-07-12 16:34:44 -04:00
arkon dfa4eda33b Remove redundant layout for reader color filter sheet
(cherry picked from commit beaffc3870)
2020-07-12 16:34:36 -04:00
arkon a229d015ad Remove color filter preview image
(cherry picked from commit 8536ecb611)
2020-07-12 16:34:26 -04:00
arkon eacdf4e161 Remove 32-bit color setting from reader sheet
(cherry picked from commit d7a89b0f8c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
#	app/src/main/res/layout/reader_settings_sheet.xml
2020-07-12 16:34:13 -04:00
arkon 9569f13190 Reorder animation speed options
(cherry picked from commit 943081e80d)
2020-07-12 16:32:14 -04:00
arkon 02ba0eca32 Update some icons
(cherry picked from commit 3f007a1edd)
2020-07-12 16:32:05 -04:00
arkon e3d2e5b89d Add option to reverse tapping (#3360)
* Add option to reverse tapping

* Fix string for preference key

* Invert tapping for Webtoon and Vertical

* Use enum instead of boolean

* Add option to reader sheet

* Hide from reader sheet if tapping disabled and remove hard coded string

* Hide option if tapping disabled

(cherry picked from commit 04d83e9a6a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt
#	app/src/main/res/layout/reader_settings_sheet.xml
2020-07-12 16:31:40 -04:00
arkon a9317dff88 Group theme settings into category
(cherry picked from commit fa5d2276c0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
2020-07-12 16:23:14 -04:00
arkon c2c2a3be01 Split general reader settings into reading mode and display
(cherry picked from commit d353a3457d)
2020-07-12 16:21:42 -04:00
arkon 57565fce2d Make page transitions setting apply to webtoon viewer as well
(cherry picked from commit b363b9fc1a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ViewerConfig.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt
#	app/src/main/res/layout/reader_settings_sheet.xml
2020-07-12 16:21:21 -04:00
arkon 439b78c39f Unify crop borders settings
(cherry picked from commit 1920568057)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
#	app/src/main/res/layout/reader_settings_sheet.xml
2020-07-12 16:16:58 -04:00
arkon fbb14a35a9 Add shortcut to global search query from library (closes #2183)
(cherry picked from commit 763da19c9d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2020-07-12 16:11:23 -04:00
arkon c0a4f4e93a Add ability to sort library by date added (closes #1287)
(cherry picked from commit 1813dbbf59)

# Conflicts:
#	app/build.gradle
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt
2020-07-12 16:08:15 -04:00
arkon c543622268 Hide invert volume keys setting when volume keys isn't enabled
(cherry picked from commit 339169b624)
2020-07-12 15:41:50 -04:00
arkon 43034db5e5 Prevent downloads when less than 50MB of disk space is available (closes #1018)
(cherry picked from commit 93960315d9)
2020-07-12 15:41:34 -04:00
arkon 27ad39b6ce Attach some FABs and snackbars to root CoordinatorLayout
Fixes some issues around snackbars sometimes being out of view.

(cherry picked from commit 479eb1ba71)

# 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-07-12 15:41:11 -04:00
arkon 04749a8fce Don't capitalize category names
(cherry picked from commit 962d8e5fd2)
2020-07-12 15:19:39 -04:00
arkon 15b23e35cd Update dependencies, remove play-services-oss-licenses
(cherry picked from commit 40639c0933)

# Conflicts:
#	app/build.gradle
2020-07-12 15:19:27 -04:00
Rani Sargees a839372d9f fix batch add log
(cherry picked from commit 629bca4100243d61cf07bbb0f47f6a1ed84031cb)
2020-07-12 15:16:52 -04:00
Jobobby04 e1fd0d1a4a Fix crash on random manga due to tracking 2020-07-11 21:03:23 -04:00
Jobobby04 6469121f41 Rewrite and enable manga cover editing, Manga info edit is finished! 2020-07-11 20:55:06 -04:00
Jobobby04 b8129ff4f6 Rewrite and enable genre tag editing 2020-07-11 18:34:16 -04:00
Jobobby04 201356afeb Reload the info once editing is done 2020-07-11 15:31:15 -04:00
Jobobby04 2e033356aa Manga info edit will now not break everything 2020-07-11 15:22:11 -04:00
Jobobby04 044c638079 Very basic manga info edit, currently will break everything if used, tags and cover edit not working 2020-07-11 14:53:59 -04:00
Jobobby04 bbf1c4ffd9 Update realms to 6.1.0, hopefully fix the startup crash with it 2020-07-11 13:37:55 -04:00
459 changed files with 15661 additions and 12600 deletions
+2 -2
View File
@@ -14,9 +14,9 @@
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs
* Include version (Setting > About > Version)
* Include version (More > About > Version)
* If not latest, try updating, it may have already been solved
* Dev version is equal to the number of commits as seen in the main page
* Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible)
+2 -2
View File
@@ -2,7 +2,7 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2)
- I have updated to the latest version of the app (stable is v1.2.0)
- 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
@@ -10,7 +10,7 @@ I acknowledge that:
---
### Device information
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
+3 -3
View File
@@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2)
- I have updated to the latest version of the app (stable is v1.2.0)
- 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
@@ -17,7 +17,7 @@ I acknowledge that:
---
### Device information
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
@@ -32,5 +32,5 @@ This should happen.
### Actual behavior
This happened instead.
### Other details
## Other details
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 have updated to the latest version of the app (stable is v0.9.2)
- I have updated to the latest version of the app (stable is v1.2.0)
- 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
@@ -17,8 +17,8 @@ I acknowledge that:
---
### Why/User Benefit/User Problem
## Why/User Benefit/User Problem
(explain why this feature should be added)
### What/Requirements
## What/Requirements
(explain how this feature would behave)
+2 -2
View File
@@ -2,7 +2,7 @@
name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
labels: "catalog"
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:
push:
branches:
- 'master'
repository_dispatch:
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
+18 -10
View File
@@ -1,6 +1,6 @@
| 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
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
Features of TachiyomiSY include:
* 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
* Hentai features enable/disable, in advanced settings
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
@@ -34,20 +33,28 @@ Features of TachiyomiSY include:
* New E-Hentai/ExHentai features, such as language settings and watched list settings
* Comfortable grid view
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them
* Manga info edit
* Enhanced views for internal and integrated 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
* Source migration, migrate all your manga from one source to another
* Custom hentai sources:
* * E-Hentai/ExHentai
* * nHentai
* * Hitomi.la
* * 8Muses
* * HBrowse
* * Perv Eden
* 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
* * Tsumino
* * HentaiCafe (Foolside)
* Saving searches
* Autoscroll
* Page preload customization
@@ -61,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
* Merge multiple of the same manga from different sources
* Drag and drop library sorting
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
## 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).
@@ -81,7 +89,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Bugs</summary>
* Include version (Setting > About > Version)
* Include version (More > About > Version)
* If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description)
+34 -28
View File
@@ -36,15 +36,14 @@ ext {
android {
compileSdkVersion AndroidConfig.compileSdk
buildToolsVersion AndroidConfig.buildTools
publishNonDefault true
defaultConfig {
applicationId "eu.kanade.tachiyomi.sy"
minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 2
versionName "1.0.0"
versionCode 6
versionName "1.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -129,7 +128,7 @@ android {
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
@@ -139,38 +138,34 @@ androidExtensions {
dependencies {
// Modified dependencies
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// AndroidX libraries
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.biometric:biometric:1.1.0-alpha01'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
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.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.webkit:webkit:1.3.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
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-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// 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-ktx:$work_version"
// UI library
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'com.google.android.material:material:1.3.0-alpha02'
standardImplementation 'com.google.firebase:firebase-core:17.4.3'
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@@ -179,11 +174,11 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// 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:logging-interceptor:$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
implementation 'org.conscrypt:conscrypt-android:2.4.0'
@@ -204,6 +199,7 @@ dependencies {
// Disk
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// HTML parser
implementation 'org.jsoup:jsoup:1.13.1'
@@ -218,10 +214,10 @@ dependencies {
implementation 'androidx.sqlite:sqlite:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.31.0'
implementation 'io.requery:sqlite-android:3.32.2'
// Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
// Model View Presenter
final nucleus_version = '3.0.0'
@@ -237,12 +233,13 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports
final acra_version = '5.5.0'
implementation "ch.acra:acra-http:$acra_version"
//implementation 'ch.acra:acra-http:5.7.0'
// Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
@@ -272,7 +269,7 @@ dependencies {
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
// FlowBinding
final flowbinding_version = '0.11.1'
final flowbinding_version = '0.12.0'
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
@@ -280,7 +277,7 @@ dependencies {
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Licenses
final aboutlibraries_version = '8.2.0'
final aboutlibraries_version = '8.3.0'
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
@@ -297,14 +294,20 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
final coroutines_version = '1.3.7'
final coroutines_version = '1.3.8'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version"
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
// Debug tool; see https://fbflipper.com/
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
// Text distance (EH)
implementation 'info.debatty:java-string-similarity:1.2.1'
@@ -338,6 +341,9 @@ dependencies {
// Humanize (EH)
implementation 'com.github.mfornos:humanize-slim:1.2.2'
// RatingBar (SY)
implementation 'me.zhanghai.android.materialratingbar:library:1.3.1'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
final def markwon_version = '4.1.0'
@@ -380,7 +386,7 @@ task copyResources(type: Copy) {
preBuild.dependsOn(ktlintFormat, copyResources)
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
apply plugin: 'com.google.gms.google-services'
// Firebase (EH)
apply plugin: 'io.fabric'
+8
View File
@@ -37,6 +37,14 @@
public *;
}
# Hitomi extension crash fix
-keepclassmembers class rx.Single {
*** onSubscribe;
final *;
protected *;
public *;
}
# RxJava 1.1.0
-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

-5
View File
@@ -39,11 +39,6 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".ui.main.ForceCloseActivity"
android:clearTaskOnLaunch="true"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask"
+9 -10
View File
@@ -6,7 +6,6 @@ import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Environment
import android.widget.Toast
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
@@ -29,11 +28,8 @@ import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.ForceCloseActivity
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.toast
import exh.debug.DebugToggles
import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay
@@ -63,11 +59,14 @@ open class App : Application(), LifecycleObserver {
workaroundAndroid7BrokenSSL()
// Enforce WebView availability
if (!WebViewUtil.supportsWebView(this)) {
toast(R.string.information_webview_required, Toast.LENGTH_LONG)
ForceCloseActivity.closeApp(this)
}
// Debug tool; see https://fbflipper.com/
// SoLoader.init(this, false)
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
// val client = AndroidFlipperClient.getInstance(this)
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
// client.addPlugin(DatabasesFlipperPlugin(this))
// client.start()
// }
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -78,6 +77,7 @@ open class App : Application(), LifecycleObserver {
Injekt.importModule(AppModule(this))
setupNotificationChannels()
Realm.init(this)
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
// Reprint.initialize(this) //Setup fingerprint (EH)
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
@@ -134,7 +134,6 @@ open class App : Application(), LifecycleObserver {
// EXH
private fun deleteOldMetadataRealm() {
Realm.init(this)
val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm")
.schemaVersion(3)
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
@@ -42,6 +43,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { CustomMangaManager(app) }
addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() }
@@ -63,5 +66,9 @@ class AppModule(val app: Application) : InjektModule {
GlobalScope.launch { get<DatabaseHelper>() }
GlobalScope.launch { get<DownloadManager>() }
// SY -->
GlobalScope.launch { get<CustomMangaManager>() }
// SY <--
}
}
@@ -87,6 +87,7 @@ object Migrations {
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
@Suppress("DEPRECATION")
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
preferences.librarySortingMode().set(LibrarySort.ALPHA)
}
@@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.annoations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw
@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
@@ -106,7 +107,7 @@ class BackupCreateService : Service() {
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
backupManager = BackupManager(this)
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
} catch (e: Exception) {
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
@@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().get())
val uri = preferences.backupsDirectory().get().toUri()
val flags = BackupCreateService.BACKUP_ALL
return try {
backupManager.createBackup(uri, flags, true)
@@ -359,7 +359,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category
// and do nothing
if (category.nameLower == dbCategory.nameLower) {
if (category.name == dbCategory.name) {
category.id = dbCategory.id
found = true
break
@@ -387,7 +387,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
if (backupCategoryStr == dbCategory.name) {
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
break
}
@@ -105,6 +105,10 @@ class BackupRestoreService : Service() {
// SY -->
private val throttleManager = EHentaiThrottleManager()
private var skippedAmount = 0
private var totalAmount = 0
// SY <--
/**
@@ -117,12 +121,6 @@ class BackupRestoreService : Service() {
*/
private var restoreAmount = 0
// SY -->
private var skippedAmount = 0
private var totalAmount = 0
// SY <--
/**
* Mapping of source ID to source name from backup data
*/
@@ -288,7 +286,7 @@ class BackupRestoreService : Service() {
backupManager.restoreSavedSearches(savedSearchesJson)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.eh_saved_searches))
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
}
// SY <--
@@ -320,13 +318,8 @@ class BackupRestoreService : Service() {
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
val message = if (manga.source in sourceMapping) {
getString(R.string.source_not_found_name, sourceMapping[manga.source])
} else {
getString(R.string.source_not_found)
}
errors.add(Date() to "${manga.title} - $message")
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
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 {
private val sourceManager: SourceManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
/**
* Checks for critical backup file data.
*
* @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 json = JsonParser.parseReader(reader).asJsonObject
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
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))
}
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> {
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
}
.toMap()
}
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
}
@@ -14,7 +14,9 @@ object MangaTypeAdapter {
write {
beginArray()
value(it.url)
value(it.title)
// SY -->
value(it.originalTitle)
// SY <--
value(it.source)
value(it.viewer)
value(it.chapter_flags)
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = /* SY --> */ 2 /* SY <-- */
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -66,6 +66,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 2) {
db.execSQL(MangaTable.addCoverLastModified)
}
if (oldVersion < 3) {
db.execSQL(MangaTable.addDateAdded)
db.execSQL(MangaTable.backfillDateAdded)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {
@@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
@@ -47,15 +48,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist)
put(COL_AUTHOR, obj.author)
put(COL_DESCRIPTION, obj.description)
put(COL_GENRE, obj.genre)
put(COL_TITLE, obj.title)
// SY -->
put(COL_ARTIST, obj.originalArtist)
put(COL_AUTHOR, obj.originalAuthor)
put(COL_DESCRIPTION, obj.originalDescription)
put(COL_GENRE, obj.originalGenre)
put(COL_TITLE, obj.originalTitle)
// SY <--
put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
@@ -64,6 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
put(COL_DATE_ADDED, obj.date_added)
}
}
@@ -85,6 +89,7 @@ interface BaseMangaGetResolver {
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
}
}
@@ -16,9 +16,6 @@ interface Category : Serializable {
var mangaOrder: List<Long>
// SY <--
val nameLower: String
get() = name.toLowerCase()
companion object {
fun create(name: String): Category = CategoryImpl().apply {
@@ -19,7 +19,6 @@ class CategoryImpl : Category {
if (other == null || javaClass != other.javaClass) return false
val category = other as Category
return name == category.name
}
@@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
return url == chapter.url
if (url != chapter.url) return false
return id == chapter.id
}
override fun hashCode(): Int {
return url.hashCode()
return url.hashCode() + id.hashCode()
}
}
@@ -12,6 +12,8 @@ interface Manga : SManga {
var last_update: Long
var date_added: Long
var viewer: Int
var chapter_flags: Int
@@ -22,10 +24,6 @@ interface Manga : SManga {
setFlags(order, SORT_MASK)
}
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC
}
@@ -34,6 +32,16 @@ interface Manga : SManga {
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) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
// Used to display the chapter's title one way or another
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK
@@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import uy.kohesive.injekt.injectLazy
open class MangaImpl : Manga {
override var id: Long? = null
@@ -9,17 +12,36 @@ open class MangaImpl : Manga {
override lateinit var url: String
// SY -->
override var title: String = ""
private val customMangaManager: CustomMangaManager by injectLazy()
override var title: String
get() = if (favorite) {
val customTitle = customMangaManager.getManga(this)?.title
if (customTitle.isNullOrBlank()) ogTitle else customTitle
} else {
ogTitle
}
set(value) {
ogTitle = value
}
override var author: String?
get() = if (favorite) customMangaManager.getManga(this)?.author ?: ogAuthor else ogAuthor
set(value) { ogAuthor = value }
override var artist: String?
get() = if (favorite) customMangaManager.getManga(this)?.artist ?: ogArtist else ogArtist
set(value) { ogArtist = value }
override var description: String?
get() = if (favorite) customMangaManager.getManga(this)?.description ?: ogDesc else ogDesc
set(value) { ogDesc = value }
override var genre: String?
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
set(value) { ogGenre = value }
// SY <--
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
@@ -28,6 +50,8 @@ open class MangaImpl : Manga {
override var last_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false
override var viewer: Int = 0
@@ -36,16 +60,29 @@ open class MangaImpl : Manga {
override var cover_last_modified: Long = 0
// SY -->
lateinit var ogTitle: String
private set
var ogAuthor: String? = null
private set
var ogArtist: String? = null
private set
var ogDesc: String? = null
private set
var ogGenre: String? = null
private set
// SY <--
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga
return url == manga.url
if (url != manga.url) return false
return id == manga.id
}
override fun hashCode(): Int {
return url.hashCode()
return url.hashCode() + id.hashCode()
}
}
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
@@ -84,6 +85,16 @@ interface MangaQueries : DbProvider {
.build()
)
.prepare()
fun updateMangaInfo(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaInfoPutResolver())
.prepare()
fun resetMangaInfo(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaInfoPutResolver(true))
.prepare()
// SY <--
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
@@ -67,7 +67,9 @@ fun getRecentsQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
WHERE ${Manga.COL_FAVORITE} = 1
AND ${Chapter.COL_DATE_UPLOAD} > ?
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
"""
@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = if (reset) resetToContentValues(manga) else mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.originalTitle)
put(MangaTable.COL_GENRE, manga.originalGenre)
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
put(MangaTable.COL_ARTIST, manga.originalArtist)
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
}
fun resetToContentValues(manga: Manga) = ContentValues(1).apply {
val splitter = "▒ ▒∩▒"
put(MangaTable.COL_TITLE, manga.title.split(splitter).last())
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull())
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull())
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull())
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull())
}
}
@@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
// [EXH]
class MangaUrlPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_URL, manga.url)
}
}
@@ -28,6 +28,8 @@ object MangaTable {
const val COL_LAST_UPDATE = "last_update"
const val COL_DATE_ADDED = "date_added"
const val COL_INITIALIZED = "initialized"
const val COL_VIEWER = "viewer"
@@ -58,7 +60,8 @@ object MangaTable {
$COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG NOT NULL
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
$COL_DATE_ADDED LONG NOT NULL
)"""
val createUrlIndexQuery: String
@@ -70,4 +73,17 @@ object MangaTable {
val addCoverLastModified: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
val addDateAdded: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
/**
* Used with addDateAdded to populate it with the oldest chapter fetch date.
*/
val backfillDateAdded: String
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)"
}
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -59,7 +59,7 @@ class DownloadCache(
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().get()
return UniFile.fromUri(context, Uri.parse(dir))
return UniFile.fromUri(context, dir.toUri())
}
/**
@@ -81,7 +81,7 @@ class DownloadCache(
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return provider.getChapterDirName(chapter) in mangaDir.files
return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
}
}
return false
@@ -122,7 +122,9 @@ class DownloadCache(
* Renews the downloads cache.
*/
private fun renew() {
val onlineSources = sourceManager.getOnlineSources()
// SY -->
val onlineSources = sourceManager.getVisibleOnlineSources()
// SY <--
val sourceDirs = rootDir.dir.listFiles()
.orEmpty()
@@ -191,12 +193,25 @@ class DownloadCache(
fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
val chapterDirName = provider.getChapterDirName(chapter)
if (chapterDirName in mangaDir.files) {
mangaDir.files -= chapterDirName
provider.getValidChapterDirNames(chapter).forEach {
if (it in mangaDir.files) {
mangaDir.files -= it
}
}
}
// 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.
*
@@ -208,9 +223,10 @@ class DownloadCache(
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
chapters.forEach { chapter ->
val chapterDirName = provider.getChapterDirName(chapter)
if (chapterDirName in mangaDir.files) {
mangaDir.files -= chapterDirName
provider.getValidChapterDirNames(chapter).forEach {
if (it in mangaDir.files) {
mangaDir.files -= it
}
}
}
}
@@ -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.download.model.Download
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.SourceManager
import eu.kanade.tachiyomi.source.model.Page
@@ -22,12 +23,10 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadManager(private val context: Context) {
class DownloadManager(/* SY private */ val context: Context) {
/**
* The sources manager.
*/
private val sourceManager by injectLazy<SourceManager>()
private val sourceManager: SourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/**
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
@@ -201,14 +200,47 @@ class DownloadManager(private val context: Context) {
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
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() }
cache.removeChapters(chapters, manga)
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
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.
*
@@ -251,16 +283,20 @@ class DownloadManager(private val context: Context) {
* @param newChapter the target chapter with the new name.
*/
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldName = provider.getChapterDirName(oldChapter)
val oldNames = provider.getValidChapterDirNames(oldChapter)
val newName = provider.getChapterDirName(newChapter)
val mangaDir = provider.getMangaDir(manga, source)
val oldFolder = mangaDir.findFile(oldName)
// Assume there's only 1 version of the chapter name formats present
val oldFolder = oldNames.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
if (oldFolder?.renameTo(newName) == true) {
cache.removeChapter(oldChapter, manga)
cache.addChapter(newName, mangaDir, manga)
} else {
Timber.e("Could not rename downloaded chapter: %s.", oldName)
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
}
}
}
@@ -13,8 +13,7 @@ import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.regex.Pattern
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@@ -23,16 +22,29 @@ import uy.kohesive.injekt.api.get
*/
internal class DownloadNotifier(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
private val preferences: PreferencesHelper by injectLazy()
private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
}
}
private val preferences by lazy { Injekt.get<PreferencesHelper>() }
private val completeNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
setAutoCancel(false)
}
}
private val errorNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
setAutoCancel(false)
}
}
/**
* Status of download. Used for correct notification icon.
*/
@Volatile
private var isDownloading = false
/**
@@ -50,14 +62,14 @@ internal class DownloadNotifier(private val context: Context) {
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
private fun NotificationCompat.Builder.show(id: Int) {
context.notificationManager.notify(id, build())
}
/**
* Clear old actions if they exist.
*/
private fun clearActions() = with(notificationBuilder) {
private fun NotificationCompat.Builder.clearActions() {
if (mActions.isNotEmpty()) {
mActions.clear()
}
@@ -67,8 +79,8 @@ internal class DownloadNotifier(private val context: Context) {
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user.
*/
fun dismiss() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
fun dismissProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
/**
@@ -77,8 +89,7 @@ internal class DownloadNotifier(private val context: Context) {
* @param download download object containing download information.
*/
fun onProgressChange(download: Download) {
// Create notification
with(notificationBuilder) {
with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
@@ -110,17 +121,16 @@ internal class DownloadNotifier(private val context: Context) {
}
setProgress(download.pages!!.size, download.downloadedImages, false)
}
// Displays the progress bar on notification
notificationBuilder.show()
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
}
/**
* Show notification when download is paused.
*/
fun onDownloadPaused() {
with(notificationBuilder) {
fun onPaused() {
with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp)
@@ -141,22 +151,45 @@ internal class DownloadNotifier(private val context: Context) {
context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
// Show notification.
notificationBuilder.show()
// Reset initial values
isDownloading = false
}
/**
* This function shows a notification to inform download tasks are done.
*/
fun onComplete() {
if (!errorThrown) {
// Create notification
with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(context.getString(R.string.download_notifier_download_finish))
setSmallIcon(android.R.drawable.stat_sys_download_done)
clearActions()
setAutoCancel(true)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
}
}
// Reset states to default
errorThrown = false
isDownloading = false
}
/**
* Called when the downloader receives a warning.
*
* @param reason the text to show.
*/
fun onWarning(reason: String) {
with(notificationBuilder) {
with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning)
@@ -164,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
}
notificationBuilder.show()
// Reset download information
isDownloading = false
@@ -180,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(notificationBuilder) {
with(errorNotificationBuilder) {
setContentTitle(
chapter
?: context.getString(R.string.download_notifier_downloader_title)
@@ -191,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
}
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information
errorThrown = true
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
/**
* Preferences used to store the list of chapters to delete.
*/
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
/**
* Last added chapter, used to avoid decoding from the preference too often.
@@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
// Last entry matches the manga, reuse it to avoid decoding json from preferences
lastEntry.copy(chapters = newChapters)
} else {
val existingEntry = prefs.getString(manga.id!!.toString(), null)
val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter
val savedEntry = gson.fromJson<Entry>(existingEntry)
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
// Save current state
val json = gson.toJson(newEntry)
prefs.edit().putString(newEntry.manga.id.toString(), json).apply()
preferences.edit {
putString(newEntry.manga.id.toString(), json)
}
lastAddedEntry = newEntry
}
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
@Synchronized
fun getPendingChapters(): Map<Manga, List<Chapter>> {
val entries = decodeAll()
prefs.edit().clear().apply()
preferences.edit {
clear()
}
lastAddedEntry = null
return entries.associate { entry ->
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
* Decodes all the chapters from preferences.
*/
private fun decodeAll(): List<Entry> {
return prefs.all.values.mapNotNull { rawEntry ->
return preferences.all.values.mapNotNull { rawEntry ->
try {
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
} catch (e: Exception) {
@@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
private data class ChapterEntry(
val id: Long,
val url: String,
val name: String
val name: String,
val scanlator: String?
)
/**
@@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
* Returns a chapter entry from a chapter model.
*/
private fun Chapter.toEntry(): ChapterEntry {
return ChapterEntry(id!!, url, name)
return ChapterEntry(id!!, url, name, scanlator)
}
/**
@@ -174,6 +180,7 @@ class DownloadPendingDeleter(context: Context) {
it.id = id
it.url = url
it.name = name
it.scanlator = scanlator
}
}
}
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads.
*/
private var downloadsDir = preferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, Uri.parse(it))
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
init {
preferences.downloadsDirectory().asFlow()
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
.launchIn(scope)
}
@@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
*/
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source)
return mangaDir?.findFile(getChapterDirName(chapter))
return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it) }
.firstOrNull()
}
/**
@@ -100,9 +102,39 @@ class DownloadProvider(private val context: Context) {
*/
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) }
return chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
}
// 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.
*
@@ -118,7 +150,9 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga to query.
*/
fun getMangaDirName(manga: Manga): String {
return DiskUtil.buildValidFilename(manga.title)
// SY -->
return DiskUtil.buildValidFilename(manga.originalTitle)
// SY <--
}
/**
@@ -127,6 +161,25 @@ class DownloadProvider(private val context: Context) {
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter): String {
return DiskUtil.buildValidFilename(chapter.name)
return DiskUtil.buildValidFilename(
when {
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
else -> chapter.name
}
)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapter the chapter to query.
*/
fun getValidChapterDirNames(chapter: Chapter): List<String> {
return listOf(
getChapterDirName(chapter),
// Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name)
)
}
}
@@ -4,6 +4,7 @@ import android.app.Notification
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
@@ -82,7 +83,7 @@ class DownloadService : Service() {
*/
override fun onCreate() {
super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
wakeLock = acquireWakeLock(javaClass.name)
runningRelay.call(true)
subscriptions = CompositeSubscription()
@@ -143,7 +144,7 @@ class DownloadService : Service() {
private fun onNetworkStateChanged(connectivity: Connectivity) {
when (connectivity.state) {
CONNECTED -> {
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) {
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
} else {
val started = downloadManager.startDownloads()
@@ -175,19 +176,19 @@ class DownloadService : Service() {
/**
* Releases the wake lock if it's held.
*/
fun PowerManager.WakeLock.releaseIfNeeded() {
private fun PowerManager.WakeLock.releaseIfNeeded() {
if (isHeld) release()
}
/**
* Acquires the wake lock if it's not held.
*/
fun PowerManager.WakeLock.acquireIfNeeded() {
private fun PowerManager.WakeLock.acquireIfNeeded() {
if (!isHeld) acquire()
}
private fun getPlaceholderNotification(): Notification {
return notification(Notifications.CHANNEL_DOWNLOADER) {
return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setContentTitle(getString(R.string.download_notifier_downloader_title))
}
}
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
@@ -42,9 +43,9 @@ class DownloadStore(
* @param downloads the list of downloads to add.
*/
fun addAll(downloads: List<Download>) {
val editor = preferences.edit()
downloads.forEach { editor.putString(getKey(it), serialize(it)) }
editor.apply()
preferences.edit {
downloads.forEach { putString(getKey(it), serialize(it)) }
}
}
/**
@@ -53,14 +54,18 @@ class DownloadStore(
* @param download the download to remove.
*/
fun remove(download: Download) {
preferences.edit().remove(getKey(download)).apply()
preferences.edit {
remove(getKey(download))
}
}
/**
* Removes all the downloads from the store.
*/
fun clear() {
preferences.edit().clear().apply()
preferences.edit {
clear()
}
}
/**
@@ -137,9 +137,10 @@ class Downloader(
} else {
if (notifier.paused) {
notifier.paused = false
notifier.onDownloadPaused()
notifier.onPaused()
} else {
notifier.dismiss()
notifier.dismissProgress()
notifier.onComplete()
}
}
}
@@ -170,7 +171,7 @@ class Downloader(
.forEach { it.status = Download.NOT_DOWNLOADED }
}
queue.clear()
notifier.dismiss()
notifier.dismissProgress()
}
/**
@@ -231,13 +232,9 @@ class Downloader(
val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source)
chapters
// Avoid downloading chapters with the same name.
.distinctBy { it.name }
// Filter out those already downloaded.
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
.filter { provider.findChapterDir(it, manga, source) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
}
@@ -270,8 +267,16 @@ class Downloader(
* @param download the chapter to be downloaded.
*/
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source)
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.ERROR
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
return@defer Observable.just(download)
}
val chapterDirname = provider.getChapterDirName(download.chapter)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) {
@@ -489,5 +494,8 @@ class Downloader(
companion object {
const val TMP_DIR_SUFFIX = "_tmp"
// Arbitrary minimum required space to start a download: 50 MB
const val MIN_DISK_SPACE = 50 * 1024 * 1024
}
}
@@ -0,0 +1,111 @@
package eu.kanade.tachiyomi.data.library
import android.content.Context
import com.github.salomonbrys.kotson.nullLong
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.set
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import java.io.File
import java.util.Scanner
class CustomMangaManager(val context: Context) {
private val editJson = File(context.getExternalFilesDir(null), "edits.json")
private var customMangaMap = mutableMapOf<Long, Manga>()
init {
fetchCustomData()
}
fun getManga(manga: Manga): Manga? = customMangaMap[manga.id]
private fun fetchCustomData() {
if (!editJson.exists() || !editJson.isFile) return
val json = try {
Gson().fromJson(
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java
)
} catch (e: Exception) {
null
} ?: return
val mangasJson = json.get("mangas").asJsonArray ?: return
customMangaMap = mangasJson.mapNotNull { element ->
val mangaObject = element.asJsonObject ?: return@mapNotNull null
val id = mangaObject["id"]?.nullLong ?: return@mapNotNull null
val manga = MangaImpl().apply {
this.id = id
title = mangaObject["title"]?.nullString ?: ""
author = mangaObject["author"]?.nullString
artist = mangaObject["artist"]?.nullString
description = mangaObject["description"]?.nullString
genre = mangaObject["genre"]?.asJsonArray?.mapNotNull { it.nullString }
?.joinToString(", ")
}
id to manga
}.toMap().toMutableMap()
}
fun saveMangaInfo(manga: MangaJson) {
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) {
customMangaMap.remove(manga.id)
} else {
customMangaMap[manga.id] = MangaImpl().apply {
id = manga.id
title = manga.title ?: ""
author = manga.author
artist = manga.artist
description = manga.description
genre = manga.genre?.joinToString(", ")
}
}
saveCustomInfo()
}
private fun saveCustomInfo() {
val jsonElements = customMangaMap.values.map { it.toJson() }
if (jsonElements.isNotEmpty()) {
val gson = GsonBuilder().create()
val root = JsonObject()
val mangaEntries = gson.toJsonTree(jsonElements)
root["mangas"] = mangaEntries
editJson.delete()
editJson.writeText(gson.toJson(root))
}
}
fun Manga.toJson(): MangaJson {
return MangaJson(
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray()
)
}
data class MangaJson(
val id: Long,
val title: String? = null,
val author: String? = null,
val artist: String? = null,
val description: String? = null,
val genre: Array<String>? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaJson
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}
}
@@ -155,7 +155,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga
* @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 manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking()
@@ -30,8 +30,12 @@ object Notifications {
/**
* Notification channel and ids used by the downloader.
*/
const val CHANNEL_DOWNLOADER = "downloader_channel"
const val ID_DOWNLOAD_CHAPTER = -201
private const val GROUP_DOWNLOADER = "group_downloader"
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
/**
@@ -50,7 +54,7 @@ object Notifications {
/**
* Notification channel and ids used by the backup/restore system.
*/
private const val GROUP_BACK_RESTORE = "group_backup_restore"
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
const val ID_BACKUP_PROGRESS = -501
const val ID_RESTORE_PROGRESS = -503
@@ -59,6 +63,7 @@ object Notifications {
const val ID_RESTORE_COMPLETE = -504
private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel"
)
@@ -70,10 +75,12 @@ object Notifications {
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore))
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup)
listOf(
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
).forEach(context.notificationManager::createNotificationChannelGroup)
val channels = listOf(
listOf(
NotificationChannel(
CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW
@@ -85,9 +92,24 @@ object Notifications {
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
@@ -99,26 +121,23 @@ object Notifications {
NotificationManager.IMPORTANCE_DEFAULT
),
NotificationChannel(
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress),
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_BACK_RESTORE
group = GROUP_BACKUP_RESTORE
setShowBadge(false)
},
NotificationChannel(
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete),
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_HIGH
).apply {
group = GROUP_BACK_RESTORE
group = GROUP_BACKUP_RESTORE
setShowBadge(false)
setSound(null, null)
}
)
context.notificationManager.createNotificationChannels(channels)
).forEach(context.notificationManager::createNotificationChannel)
// Delete old notification channels
deprecatedChannels.forEach {
context.notificationManager.deleteNotificationChannel(it)
}
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
}
}
@@ -55,6 +55,8 @@ object PreferenceKeys {
const val readWithTapping = "reader_tap"
const val readWithTappingInverted = "reader_tapping_inverted"
const val readWithLongTap = "reader_long_tap"
const val readWithVolumeKeys = "reader_volume_keys"
@@ -67,6 +69,8 @@ object PreferenceKeys {
const val landscapeColumns = "pref_library_columns_landscape_key"
const val jumpToChapters = "jump_to_chapters"
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
@@ -93,6 +97,8 @@ object PreferenceKeys {
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 libraryUpdateRestriction = "library_update_restriction"
@@ -117,6 +123,8 @@ object PreferenceKeys {
const val automaticExtUpdates = "automatic_ext_updates"
const val allowNsfwSource = "allow_nsfw_source"
const val startScreen = "start_screen"
const val useBiometricLock = "use_biometric_lock"
@@ -179,8 +187,6 @@ object PreferenceKeys {
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_readOnlySync = "eh_sync_read_only"
@@ -233,8 +239,6 @@ object PreferenceKeys {
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_tag_filtering_value = "eh_tag_filtering_value"
@@ -243,8 +247,6 @@ object PreferenceKeys {
const val eh_is_hentai_enabled = "eh_is_hentai_enabled"
const val eh_use_new_manga_interface = "eh_use_new_manga_interface"
const val eh_use_auto_webtoon = "eh_use_auto_webtoon"
const val eh_watched_list_default_state = "eh_watched_list_default_state"
@@ -268,4 +270,14 @@ object PreferenceKeys {
const val sources_tab_source_categories = "sources_tab_source_categories"
const val sourcesSort = "sources_sort"
const val recommendsInOverflow = "recommends_in_overflow"
const val enhancedEHentaiView = "enhanced_e_hentai_view"
const val webtoonEnableZoomOut = "webtoon_enable_zoom_out"
const val startReadingButton = "start_reading_button"
const val groupLibraryBy = "group_library_by"
}
@@ -30,4 +30,17 @@ object PreferenceValues {
COMFORTABLE_GRID,
LIST,
}
enum class TappingInvertMode {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
}
}
@@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference
@@ -10,6 +10,7 @@ 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.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.anilist.Anilist
import java.io.File
@@ -27,27 +28,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
.onEach { block(it) }
}
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item)
}
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
@OptIn(ExperimentalCoroutinesApi::class)
class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile(
File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
)
)
private val defaultDownloadsDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
).toUri()
private val defaultBackupDir = Uri.fromFile(
File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
)
)
private val defaultBackupDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
).toUri()
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
@@ -109,7 +114,7 @@ class PreferencesHelper(val context: Context) {
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)
@@ -121,6 +126,8 @@ class PreferencesHelper(val context: Context) {
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
@@ -131,6 +138,8 @@ class PreferencesHelper(val context: Context) {
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
@@ -179,6 +188,8 @@ class PreferencesHelper(val context: Context) {
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
@@ -214,6 +225,8 @@ class PreferencesHelper(val context: Context) {
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
@@ -299,8 +312,6 @@ class PreferencesHelper(val context: Context) {
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
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_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
@@ -343,12 +354,8 @@ class PreferencesHelper(val context: Context) {
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_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true)
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false)
@@ -368,4 +375,14 @@ class PreferencesHelper(val context: Context) {
fun sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
fun recommendsInOverflow() = flowPrefs.getBoolean(Keys.recommendsInOverflow, false)
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
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.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) {
prefs.edit().putBoolean(key, value).apply()
prefs.edit {
putBoolean(key, value)
}
}
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) {
prefs.edit().putInt(key, value).apply()
prefs.edit {
putInt(key, value)
}
}
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) {
prefs.edit().putLong(key, value).apply()
prefs.edit {
putLong(key, value)
}
}
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) {
prefs.edit().putFloat(key, value).apply()
prefs.edit {
putFloat(key, value)
}
}
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?) {
prefs.edit().putString(key, value).apply()
prefs.edit {
putString(key, value)
}
}
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>?) {
prefs.edit().putStringSet(key, values).apply()
prefs.edit {
putStringSet(key, values)
}
}
}
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject
@@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return baseMangaUrl + mediaId
}
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon()
fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token")
.build()
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
@@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse(
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
).buildUpon()
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
.toUri()
.buildUpon()
.appendQueryParameter("max_results", "20")
.build()
val request = Request.Builder()
@@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return "$baseMangaUrl/$remoteId"
}
fun authUrl() =
Uri.parse(loginUrl).buildUpon()
fun authUrl(): Uri =
loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl)
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
@@ -260,13 +260,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun loginUrl() = Uri.parse(baseUrl).buildUpon()
private fun loginUrl() = baseUrl.toUri().buildUpon()
.appendPath("login.php")
.toString()
private fun searchUrl(query: String): String {
val col = "c[]"
return Uri.parse(baseUrl).buildUpon()
return baseUrl.toUri().buildUpon()
.appendPath("manga.php")
.appendQueryParameter("q", query)
.appendQueryParameter(col, "a")
@@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.toString()
}
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon()
private fun exportListUrl() = baseUrl.toUri().buildUpon()
.appendPath("panel.php")
.appendQueryParameter("go", "export")
.toString()
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
.appendPath(mediaId.toString())
.appendPath("edit")
.toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
.appendPath("add.json")
.toString()
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt()
if (numScore in 1..9) {
if (numScore == 0) {
score = ""
} else if (numScore in 1..10) {
score = numScore.toString()
}
status = track.status.toString()
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullString
@@ -54,7 +54,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse("$apiUrl/mangas").buildUpon()
val url = "$apiUrl/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
@@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
}
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
@@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.get()
.build()
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build()
val requestMangas = Request.Builder()
@@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
}
fun authUrl() =
Uri.parse(loginUrl).buildUpon()
loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code")
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
override suspend fun checkForUpdate(): UpdateResult {
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"
@@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@@ -18,14 +19,8 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.HBROWSE_SOURCE_ID
import exh.HITOMI_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 kotlinx.coroutines.async
import rx.Observable
@@ -83,12 +78,6 @@ class ExtensionManager(
return when (source.id) {
EH_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)
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_source)
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
else -> null
}
@@ -319,8 +308,7 @@ class ExtensionManager(
if (signature !in untrustedSignatures) return
ExtensionLoader.trustedSignatures += signature
val preference = preferences.trustedSignatures()
preference.set(preference.get() + signature)
preferences.trustedSignatures() += signature
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
untrustedExtensions -= nowTrustedExtensions
@@ -1,44 +1,30 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
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 java.util.Date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
private val network: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val gson: Gson by injectLazy()
suspend fun findExtensions(): List<Extension.Available> {
val call = GET(EXT_URL)
val service: ExtensionGithubService = ExtensionGithubService.create()
return withContext(Dispatchers.IO) {
val response = network.client.newCall(call).await()
if (response.isSuccessful) {
parseResponse(response)
} else {
response.close()
throw Exception("Failed to get extensions")
}
val response = service.getRepo()
parseResponse(response)
}
}
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate
}
private fun parseResponse(response: Response): List<Extension.Available> {
val text = response.body?.use { it.string() } ?: return emptyList()
val json = gson.fromJson<JsonArray>(text)
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element["version"].string
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
val versionName = element["version"].string
val versionCode = element["code"].int
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 {
return "$REPO_URL/apk/${extension.apkName}"
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
// SY -->
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
// SY <--
companion object {
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo"
private const val EXT_URL = "$REPO_URL/index.json"
const val BASE_URL = "https://raw.githubusercontent.com/"
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 versionCode: Int
abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val sources: List<Source>,
override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
@@ -31,6 +33,7 @@ sealed class Extension {
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
) : Extension()
@@ -41,6 +44,7 @@ sealed class Extension {
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null
override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension()
}
@@ -7,6 +7,8 @@ import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@@ -63,7 +65,7 @@ internal class ExtensionInstaller(private val context: Context) {
// Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register()
val downloadUri = Uri.parse(url)
val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
@@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param pkgName The package name of the extension to uninstall
*/
fun uninstallApk(pkgName: String) {
val packageUri = Uri.parse("package:$pkgName")
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
@@ -5,6 +5,8 @@ import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
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.extension.model.Extension
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.runBlocking
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
@SuppressLint("PackageManagerGetSignatures")
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 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_MAX = 1.2
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
var trustedSignatures = mutableSetOf<String>() +
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
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 sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) {
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}")
}
} catch (e: Throwable) {
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
return LoadResult.Error(e)
}
}
.filter { !isSourceNsfw(it) }
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
}
val extension = Extension.Installed(
extName, pkgName, versionName, versionCode, sources, lang,
extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
isUnofficial = signatureHash != officialSignature
)
return LoadResult.Success(extension)
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
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,19 @@ package eu.kanade.tachiyomi.network
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
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.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException
import java.util.concurrent.CountDownLatch
@@ -42,9 +43,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
}
return chain.proceed(originalRequest)
}
initWebView
val originalRequest = chain.request()
val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on
@@ -85,7 +94,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
handler.post {
val webview = WebView(context)
webView = webview
webview.settings.javaScriptEnabled = true
webview.setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent")
@@ -105,7 +114,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
}
// 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
) {
// The first request didn't return the challenge, abort.
@@ -113,13 +122,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
}
}
override fun onReceivedHttpError(
override fun onReceivedErrorCompat(
view: WebView,
request: WebResourceRequest,
errorResponse: WebResourceResponse
errorCode: Int,
description: String?,
failingUrl: String,
isMainFrame: Boolean
) {
if (request.isForMainFrame) {
if (errorResponse.statusCode == 503) {
if (isMainFrame) {
if (errorCode == 503) {
// Found the Cloudflare challenge page.
challengeFound = true
} else {
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source
import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
@@ -28,13 +29,15 @@ import timber.log.Timber
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
private val POPULAR_FILTERS = FilterList(OrderBy())
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
const val ID = 0L
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
val dir = getBaseDirectories(context).firstOrNull()
@@ -73,9 +76,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
val baseDirs = getBaseDirectories(context)
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() }
var mangaDirs = baseDirs
.asSequence()
.mapNotNull { it.listFiles()?.toList() }
.flatten()
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.filter { it.isDirectory }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
@@ -132,13 +138,55 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
}
}
return Observable.just(MangasPage(mangas, false))
return Observable.just(MangasPage(mangas.toList(), false))
}
// SY -->
fun updateMangaInfo(manga: SManga) {
val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find {
it.exists()
} ?: return
val gson = GsonBuilder().setPrettyPrinting().create()
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
val file = File(directory, existingFileName ?: "info.json")
file.writeText(gson.toJson(manga.toJson()))
}
fun SManga.toJson(): MangaJson {
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray())
}
data class MangaJson(
val title: String,
val author: String?,
val artist: String?,
val description: String?,
val genre: Array<String>?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MangaJson
if (title != other.title) return false
return true
}
override fun hashCode(): Int {
return title.hashCode()
}
}
// SY <--
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten()
.firstOrNull { it.extension == "json" }
@@ -154,6 +202,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
?: manga.genre
manga.status = json["status"]?.asInt ?: manga.status
}
return Observable.just(manga)
}
@@ -204,8 +253,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
var chapterNameIndex = 0
var mangaTitleIndex = 0
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
val chapterChar = chapterName.get(chapterNameIndex)
val mangaChar = mangaTitle.get(mangaTitleIndex)
val chapterChar = chapterName[chapterNameIndex]
val mangaChar = mangaTitle[mangaTitleIndex]
if (!chapterChar.equals(mangaChar, true)) {
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
@@ -235,7 +284,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
private fun isSupportedFile(extension: String): Boolean {
return extension.toLowerCase() in setOf("zip", "rar", "cbr", "cbz", "epub")
return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
}
fun getFormat(chapter: SChapter): Format {
@@ -269,8 +318,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
return when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) }
}
@@ -4,6 +4,7 @@ import android.content.Context
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@@ -19,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.Tsumino
import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_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_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.DelegatedHttpSource
import exh.source.EnhancedHttpSource
@@ -31,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import rx.Observable
import uy.kohesive.injekt.injectLazy
@@ -52,7 +56,7 @@ open class SourceManager(private val context: Context) {
// SY -->
// Recreate sources when they change
prefs.enableExhentai().asFlow().onEach {
prefs.enableExhentai().asImmediateFlow {
createEHSources().forEach { registerSource(it) }
}.launchIn(scope)
@@ -72,6 +76,10 @@ open class SourceManager(private val context: Context) {
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>()
fun getVisibleOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>().filter {
it.id !in BlacklistedSources.HIDDEN_SOURCES
}
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
// SY -->
@@ -98,9 +106,9 @@ open class SourceManager(private val context: Context) {
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
val enhancedSource = EnhancedHttpSource(
source,
delegate.newSourceClass.constructors.find { it.parameters.size == 1 }!!.call(source)
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)
enhancedSource
} else source
@@ -132,12 +140,6 @@ open class SourceManager(private val context: Context) {
if (prefs.enableExhentai().get()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context)
}
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
exSrcs += NHentai(context)
exSrcs += Hitomi()
exSrcs += EightMuses()
exSrcs += HBrowse()
return exSrcs
}
// SY <--
@@ -170,23 +172,23 @@ open class SourceManager(private val context: Context) {
// SY -->
companion object {
private const val fillInSourceId = 9999L
private const val fillInSourceId = Long.MAX_VALUE
val DELEGATED_SOURCES = listOf(
DelegatedSource(
"Hentai Cafe",
260868874183818481,
HENTAI_CAFE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
HentaiCafe::class
),
DelegatedSource(
"Pururin",
2221515250486218861,
PURURIN_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.pururin.Pururin",
Pururin::class
),
DelegatedSource(
"Tsumino",
6707338697138388238,
TSUMINO_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class
)/*,
@@ -196,10 +198,48 @@ open class SourceManager(private val context: Context) {
"eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class,
true
)*/
)*/,
DelegatedSource(
"HBrowse",
HBROWSE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.hbrowse.HBrowse",
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 }
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>()
var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
data class DelegatedSource(
val sourceName: String,
@@ -29,7 +29,9 @@ sealed class Filter<T>(val name: String, var state: T) {
data class Selection(val index: Int, val ascending: Boolean)
}
// SY -->
abstract class AutoComplete(name: String, val hint: String, val values: List<String>, val skipAutoFillTags: List<String> = emptyList(), val excludePrefix: String? = null, state: List<String>) : Filter<List<String>>(name, state)
// SY <--
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -1,3 +1,9 @@
package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
import exh.metadata.metadata.base.RaisedSearchMetadata
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)
// SY -->
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
// SY <--
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import java.io.Serializable
interface SManga : Serializable {
@@ -22,27 +23,40 @@ interface SManga : Serializable {
var initialized: Boolean
// SY -->
val originalTitle: String
get() = (this as? MangaImpl)?.ogTitle ?: title
val originalAuthor: String?
get() = (this as? MangaImpl)?.ogAuthor ?: author
val originalArtist: String?
get() = (this as? MangaImpl)?.ogArtist ?: artist
val originalDescription: String?
get() = (this as? MangaImpl)?.ogDesc ?: description
val originalGenre: String?
get() = (this as? MangaImpl)?.ogGenre ?: genre
// SY <--
fun copyFrom(other: SManga) {
// EXH -->
if (other.title.isNotBlank()) {
title = other.title
title = other.originalTitle
}
// EXH <--
if (other.author != null) {
author = other.author
author = /* SY --> */ other.originalAuthor /* SY <-- */
}
if (other.artist != null) {
artist = other.artist
artist = /* SY --> */ other.originalArtist /* SY <-- */
}
if (other.description != null) {
description = other.description
description = /* SY --> */ other.originalDescription /* SY <-- */
}
if (other.genre != null) {
genre = other.genre
genre = /* SY --> */ other.originalGenre /* SY <-- */
}
if (other.thumbnail_url != null) {
@@ -61,9 +75,6 @@ interface SManga : Serializable {
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
// SY -->
const val RECOMMENDS = 69 // nice
// SY <--
fun create(): SManga {
return SMangaImpl()
@@ -1,14 +1,18 @@
package eu.kanade.tachiyomi.source.online
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.source.EnhancedHttpSource
import kotlin.reflect.KClass
import rx.Completable
import rx.Single
@@ -102,6 +106,24 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
}
}
fun getDescriptionAdapter(controller: MangaController): RecyclerView.Adapter<*>?
val SManga.id get() = (this as? Manga)?.id
val SChapter.mangaId get() = (this as? Chapter)?.manga_id
companion object {
fun Source.isLewdSource() = (this is LewdSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>))
fun Source.getLewdSource(): LewdSource<*, *>? {
return if (!this.isLewdSource()) {
null
} else if (this is LewdSource<*, *>) {
this
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) {
this.enhancedSource
} else {
null
}
}
}
}
@@ -18,13 +18,14 @@ import eu.kanade.tachiyomi.network.GET
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.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.debug.DebugToggles
import exh.eh.EHTags
@@ -36,15 +37,19 @@ import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
import exh.metadata.metadata.base.RaisedTag
import exh.metadata.nullIfBlank
import exh.metadata.parseHumanReadableByteCount
import exh.ui.login.LoginController
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
import exh.util.UriFilter
import exh.util.UriGroup
import exh.util.asObservableWithAsyncStacktrace
import exh.util.ignore
import exh.util.nullIfBlank
import exh.util.trimOrNull
import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder
import java.util.ArrayList
@@ -91,9 +96,9 @@ class EHentai(
/**
* Gallery list entry
*/
data class ParsedManga(val fav: Int, val manga: Manga)
data class ParsedManga(val fav: Int, val manga: Manga, val metadata: EHentaiSearchMetadata)
fun extendedGenericMangaParse(doc: Document) = with(doc) {
private fun extendedGenericMangaParse(doc: Document) = with(doc) {
// Parse mangas (supports compact + extended layout)
val parsedMangas = select(".itg > tbody > tr").filter {
// Do not parse header and ads
@@ -102,8 +107,11 @@ class EHentai(
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
val column2 = it.selectFirst(".gl3e, .gl2c")
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a")
val infoElement = it.selectFirst(".gl3e")
val favElement = column2.children().find { it.attr("style").startsWith("border-color") }
val infoElements = infoElement?.select("div")
val parsedTags = mutableListOf<RaisedTag>()
ParsedManga(
fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
@@ -116,7 +124,78 @@ class EHentai(
// Get image
thumbnail_url = thumbnailElement.attr("src")
// TODO Parse genre + uploader + tags
if (infoElements != null) {
linkElement.select("div div")?.getOrNull(1)?.select("tr")?.forEach { row ->
val namespace = row.select(".tc").text().removeSuffix(":")
parsedTags.addAll(
row.select("div").map { element ->
RaisedTag(
namespace,
element.text().trim(),
when {
element.hasClass("gtl") -> TAG_TYPE_LIGHT
element.hasClass("gtw") -> TAG_TYPE_WEAK
else -> TAG_TYPE_NORMAL
}
)
}
)
}
} else {
val tagElement = it.selectFirst(".gl3c > a")
val tagElements = tagElement.select("div")
tagElements.forEach { element ->
if (element.className() == "gt") {
val namespace = element.attr("title").substringBefore(":").trimOrNull() ?: "misc"
parsedTags += RaisedTag(
namespace,
element.attr("title").substringAfter(":").trim(),
TAG_TYPE_NORMAL
)
}
}
}
genre = parsedTags.toGenreString()
},
metadata = EHentaiSearchMetadata().apply {
tags += parsedTags
if (infoElements != null) {
getGenre(infoElements.getOrNull(1))?.let { genre = it }
getDateTag(infoElements.getOrNull(2))?.let { datePosted = it }
getRating(infoElements.getOrNull(3))?.let { averageRating = it }
getUploader(infoElements.getOrNull(4))?.let { uploader = it }
getPageCount(infoElements.getOrNull(5))?.let { length = it }
} else {
val parsedGenre = it.selectFirst(".gl1c div")
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
val info = it.selectFirst(".gl2c")
val extraInfo = it.selectFirst(".gl4c")
val infoList = info.select("div div")
getDateTag(infoList.getOrNull(8))?.let { datePosted = it }
getRating(infoList.getOrNull(9))?.let { averageRating = it }
val extraInfoList = extraInfo.select("div")
if (extraInfoList.getOrNull(2) == null) {
getUploader(extraInfoList.getOrNull(0))?.let { uploader = it }
getPageCount(extraInfoList.getOrNull(1))?.let { length = it }
} else {
getUploader(extraInfoList.getOrNull(1))?.let { uploader = it }
getPageCount(extraInfoList.getOrNull(2))?.let { length = it }
}
}
}
)
}
@@ -136,11 +215,54 @@ class EHentai(
Pair(parsedMangas, hasNextPage)
}
private fun getGenre(element: Element? = null, genreString: String? = null): String? {
return element?.attr("onclick")
?.nullIfBlank()
?.substringAfterLast('/')
?.removeSuffix("'")
?.trim()
?.substringAfterLast('/')
?.removeSuffix("'") ?: genreString
}
private fun getDateTag(element: Element?): Long? {
val text = element?.text()?.nullIfBlank()
return if (text != null) {
val date = EX_DATE_FORMAT.parse(text)
date?.time
} else null
}
private fun getRating(element: Element?): Double? {
val ratingStyle = element?.attr("style")?.nullIfBlank()
return if (ratingStyle != null) {
val matches = RATING_REGEX.findAll(ratingStyle).mapNotNull { it.groupValues.getOrNull(1)?.toIntOrNull() }.toList()
if (matches.size == 2) {
var rate = 5 - matches[0] / 16
if (matches[1] == 21) {
rate--
rate + 0.5
} else rate.toDouble()
} else null
} else null
}
private fun getUploader(element: Element?): String? {
return element?.select("a")?.text()?.trimOrNull()
}
private fun getPageCount(element: Element?): Int? {
val pageCount = element?.text()?.trimOrNull()
return if (pageCount != null) {
PAGE_COUNT_REGEX.find(pageCount)?.value?.toIntOrNull()
} else null
}
/**
* Parse a list of galleries
*/
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let {
MangasPage(it.first.map { it.manga }, it.second)
MetadataMangasPage(it.first.map { it.manga }, it.second, it.first.map { it.metadata })
}
override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
@@ -271,7 +393,7 @@ class EHentai(
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
urlImportFetchSearchManga(context, query) {
searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess()
}.map { response ->
@@ -477,6 +599,8 @@ class EHentai(
element.text().trim(),
if (element.hasClass("gtl")) {
TAG_TYPE_LIGHT
} else if (element.hasClass("gtw")) {
TAG_TYPE_WEAK
} else {
TAG_TYPE_NORMAL
}
@@ -550,7 +674,7 @@ class EHentai(
page++
} while (parsed.second)
return Pair(result as List<ParsedManga>, favNames!!)
return Pair(result.toList(), favNames!!)
}
fun spPref() = if (exh) {
@@ -832,10 +956,16 @@ class EHentai(
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
}
override fun getDescriptionAdapter(controller: MangaController): EHentaiDescriptionAdapter {
return EHentaiDescriptionAdapter(controller)
}
companion object {
private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
private const val TR_SUFFIX = "TR"
private const val REVERSE_PARAM = "TEH_REVERSE"
private val PAGE_COUNT_REGEX = "[0-9]*".toRegex()
private val RATING_REGEX = "([0-9]*)px".toRegex()
private const val EH_API_BASE = "https://api.e-hentai.org/api.php"
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
@@ -1,99 +1,51 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
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.source.model.FilterList
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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
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.Companion.BASE_URL
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.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.vepta.vdm.ByteCursor
import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
/**
* Man, I hate this source :(
*/
class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, 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
*/
class Hitomi(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<HitomiSearchMetadata, Document>,
UrlImportableSource {
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
private var tagIndexVersionCacheTime: Long = 0
private fun tagIndexVersion(): Single<Long> {
val sCachedTagIndexVersion = cachedTagIndexVersion
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)
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
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) {
with(metadata) {
url = input.location()
@@ -106,306 +58,63 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
title = galleryElement.selectFirst("h1").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 {
val content = it.child(1)
when (it.child(0).text().toLowerCase()) {
"group" -> {
group = content.text()
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL)
tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
}
"type" -> {
type = content.text()
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL)
tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
}
"series" -> {
series = content.select("a").map { it.text() }
tags += series.map {
RaisedTag("series", it, TAG_TYPE_VIRTUAL)
RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
}
}
"language" -> {
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
language?.let {
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL)
tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
}
}
"characters" -> {
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 += content.select("a").map {
val ns = if (it.attr("href").startsWith("/tag/male")) "male"
else if (it.attr("href").startsWith("/tag/female")) "female"
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"
/**
* 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(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("data-srcset").substringBefore(' ')
} else {
doc.selectFirst("img").attr("data-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") jsonElement["name"].string.split('.').last() else "webp"
val path = if (jsonElement["haswebp"].string == "0") "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 fun toString() = "${delegate.name} (${lang.toUpperCase()})"
override val matchingHosts = listOf(
"hitomi.la"
@@ -421,11 +130,12 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
}
companion object {
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10
private val PAGE_SIZE = 25
private val NUMBER_OF_FRONTENDS = 2
override fun getDescriptionAdapter(controller: MangaController): HitomiDescriptionAdapter {
return HitomiDescriptionAdapter(controller)
}
companion object {
const val otherId = 2703068117101782422L
private val DATE_FORMAT by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -11,7 +12,7 @@ import exh.source.DelegatedHttpSource
import exh.util.urlImportFetchSearchManga
import rx.Observable
class MangaDex(delegate: HttpSource) :
class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
ConfigurableSource,
UrlImportableSource {
@@ -19,7 +20,7 @@ class MangaDex(delegate: HttpSource) :
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(query) {
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
+25 -279
View File
@@ -8,112 +8,38 @@ import com.github.salomonbrys.kotson.nullLong
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
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.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup
import exh.NHENTAI_SOURCE_ID
import eu.kanade.tachiyomi.ui.manga.MangaController
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.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import rx.Observable
/**
* NHentai source
*/
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
open class NHentai(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<NHentaiSearchMetadata, Response>,
UrlImportableSource {
override val metaClass = NHentaiSearchMetadata::class
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
// TODO There is currently no way to get the most popular mangas
// 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()
override val lang = if (delegate.lang == "other") "all" else delegate.lang
override val id: Long
get() = if (delegate.lang == "other") otherId else delegate.id
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val trimmedIdQuery = query.trim().removePrefix("id:")
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) {
"$baseUrl/g/$trimmedIdQuery/"
} else query
return urlImportFetchSearchManga(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())
}
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
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> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
@@ -128,37 +54,10 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
}
}
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) {
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
with(metadata) {
@@ -195,164 +94,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
tags.clear()
}?.forEach {
if (it.first != null && it.second != null) {
tags.add(RaisedTag(it.first!!, it.second!!, 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) {
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 fun toString() = "${delegate.name} (${lang.toUpperCase()})"
override val matchingHosts = listOf(
"nhentai.net"
@@ -366,16 +114,14 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
return "$baseUrl/g/${uri.pathSegments[1]}/"
}
override fun getDescriptionAdapter(controller: MangaController): NHentaiDescriptionAdapter {
return NHentaiDescriptionAdapter(controller)
}
companion object {
const val otherId = 7309872737163460316L
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
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")
)
}
}
+26 -251
View File
@@ -1,153 +1,48 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.GET
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.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.online.HttpSource
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.ui.manga.MangaController
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.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.util.UriFilter
import exh.util.UriGroup
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
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.Element
import org.jsoup.nodes.TextNode
import rx.Observable
// TODO Transform into delegated source
class PervEden(override val id: Long, val pvLang: PervEdenLang) :
ParsedHttpSource(),
class PervEden(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<PervEdenSearchMetadata, Document>,
UrlImportableSource {
/**
* The class of the metadata used by this source
*/
override val metaClass = PervEdenSearchMetadata::class
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
override val lang = delegate.lang
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
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> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
with(metadata) {
url = Uri.parse(input.location()).path
@@ -181,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
"Artist" -> {
if (it is Element && it.tagName() == "a") {
artist = it.text()
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL)
tags += RaisedTag(
"artist", it.text().toLowerCase(),
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
)
}
}
"Genres" -> {
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" -> {
@@ -215,137 +116,13 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
}
}
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 fun matchesUri(uri: Uri): Boolean {
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) {
PervEdenLang.en -> "en-manga"
PervEdenLang.it -> "it-manga"
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
"en" -> "en-manga"
"it" -> "it-manga"
else -> false
}
}
@@ -357,9 +134,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
return newUri.toString()
}
companion object {
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply {
timeZone = TimeZone.getTimeZone("GMT")
}
override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
return PervEdenDescriptionAdapter(controller)
}
}
@@ -1,250 +1,38 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
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.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.EIGHTMUSES_SOURCE_ID
import exh.metadata.metadata.EightMusesSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.util.CachedField
import exh.util.NakedTrie
import exh.util.await
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
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.Element
import rx.Observable
import rx.schedulers.Schedulers
typealias SiteMap = NakedTrie<Unit>
class EightMuses :
HttpSource(),
class EightMuses(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<EightMusesSearchMetadata, Document>,
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 lang = "en"
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
override val baseUrl = EightMusesSearchMetadata.BASE_URL
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()
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
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(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> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
@@ -253,46 +41,6 @@ class EightMuses :
}
}
/**
* 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().await(Schedulers.io()).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>)
private fun parseSelf(doc: Document): SelfContents {
@@ -306,22 +54,6 @@ class EightMuses :
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) {
with(metadata) {
path = Uri.parse(input.location()).pathSegments
@@ -352,40 +84,9 @@ class EightMuses :
}
}
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(
"www.8muses.com",
"comics.8muses.com",
"8muses.com"
)
@@ -396,4 +97,8 @@ class EightMuses :
}
return "/comics/album/${path.joinToString("/")}"
}
override fun getDescriptionAdapter(controller: MangaController): EightMusesDescriptionAdapter {
return EightMusesDescriptionAdapter(controller)
}
}
@@ -1,328 +1,55 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
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.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.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.HBROWSE_SOURCE_ID
import exh.metadata.metadata.HBrowseSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.search.Namespace
import exh.search.SearchEngine
import exh.search.Text
import exh.util.await
import exh.util.dropBlank
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import info.debatty.java.stringsimilarity.Levenshtein
import kotlin.math.ceil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import okhttp3.CookieJar
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import rx.schedulers.Schedulers
class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlImportableSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang: String = "en"
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
override val baseUrl = HBrowseSearchMetadata.BASE_URL
override val name: String = "HBrowse"
override val supportsLatest = true
class HBrowse(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<HBrowseSearchMetadata, Document>,
UrlImportableSource {
override val metaClass = HBrowseSearchMetadata::class
override val lang = "en"
override val id: Long = HBROWSE_SOURCE_ID
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
override fun headersBuilder() = Headers.Builder()
.add("Cookie", BASE_COOKIES)
private val clientWithoutCookies = client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES)
.build()
private val nonRedirectingClientWithoutCookies = clientWithoutCookies.newBuilder()
.followRedirects(false)
.build()
private val searchEngine = SearchEngine()
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int) = GET("$baseUrl/browse/title/rank/DESC/$page", headers)
private fun parseListing(response: Response): MangasPage {
val doc = response.asJsoup()
val main = doc.selectFirst("#main")
val items = main.select(".thumbTable > tbody")
val manga = items.map { mangaEle ->
SManga.create().apply {
val thumbElement = mangaEle.selectFirst(".thumbImg")
url = "/" + thumbElement.parent().attr("href").split("/").dropBlank().first()
title = thumbElement.parent().attr("title").substringAfter('\'').substringBeforeLast('\'')
thumbnail_url = baseUrl + thumbElement.attr("src")
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
}
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
return MangasPage(
manga,
hasNextPage
)
}
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return urlImportFetchSearchManga(query) {
fetchSearchMangaInternal(page, query, filters)
}
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response) = parseListing(response)
/**
* 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("Should not be called!")
private fun fetchSearchMangaInternal(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
val modeFilter = filters.filterIsInstance<ModeFilter>().firstOrNull()
val sortFilter = filters.filterIsInstance<SortFilter>().firstOrNull()
var base: String? = null
var isSortFilter = false
// <NS, VALUE, EXCLUDED>
var tagQuery: List<Triple<String, String, Boolean>>? = null
if (sortFilter != null) {
sortFilter.state?.let { state ->
if (query.isNotBlank()) {
throw IllegalArgumentException("Cannot use sorting while text/tag search is active!")
}
isSortFilter = true
base = "/browse/title/${SortFilter.SORT_OPTIONS[state.index].first}/${if (state.ascending) "ASC" else "DESC"}"
}
}
if (base == null) {
base = if (modeFilter != null && modeFilter.state == 1) {
tagQuery = searchEngine.parseQuery(query, false).map {
when (it) {
is Text -> {
var minDist = Int.MAX_VALUE.toDouble()
// ns, value
var minContent: Pair<String, String> = "" to ""
for (ns in ALL_TAGS) {
val (v, d) = ns.value.nearest(it.rawTextOnly(), minDist)
if (d < minDist) {
minDist = d
minContent = ns.key to v
}
}
minContent
}
is Namespace -> {
// Map ns aliases
val mappedNs = NS_MAPPINGS[it.namespace] ?: it.namespace
var key = mappedNs
if (!ALL_TAGS.containsKey(key)) key = ALL_TAGS.keys.sorted().nearest(mappedNs).first
// Find nearest NS
val nsContents = ALL_TAGS[key]
key to nsContents!!.nearest(it.tag?.rawTextOnly() ?: "").first
}
else -> error("Unknown type!")
}.let { p ->
Triple(p.first, p.second, it.excluded)
}
}
"/result"
} else {
"/search"
}
}
base += "/$page"
if (isSortFilter) {
parseListing(
client.newCall(GET(baseUrl + base, headers))
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
)
} else {
val body = if (tagQuery != null) {
FormBody.Builder()
.add("type", "advance")
.apply {
tagQuery.forEach {
add(it.first + "_" + it.second, if (it.third) "n" else "y")
}
}
} else {
FormBody.Builder()
.add("type", "search")
.add("needle", query)
}
val processRequest = POST(
"$baseUrl/content/process.php",
headers,
body = body.build()
)
val processResponse = nonRedirectingClientWithoutCookies.newCall(processRequest)
.asObservable()
.toSingle()
.await(Schedulers.io())
if (!processResponse.isRedirect) {
throw IllegalStateException("Unexpected process response code!")
}
val sessId = processResponse.headers("Set-Cookie").find {
it.startsWith("PHPSESSID")
} ?: throw IllegalStateException("Missing server session cookie!")
val response = clientWithoutCookies.newCall(
GET(
baseUrl + base,
headersBuilder()
.set("Cookie", BASE_COOKIES + " " + sessId.substringBefore(';'))
.build()
)
)
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
val doc = response.asJsoup()
val manga = doc.select(".browseDescription").map {
SManga.create().apply {
val first = it.child(0)
url = first.attr("href")
title = first.attr("title").substringAfter('\'').removeSuffix("'").replace('_', ' ')
thumbnail_url = HBrowseSearchMetadata.guessThumbnailUrl(url.substring(1))
}
}
val hasNextPage = doc.selectFirst("#main > p > a[title~=jump]:nth-last-child(1)") != null
MangasPage(
manga,
hasNextPage
)
}
}.asSingle(GlobalScope.coroutineContext)
).toObservable()
}
// Collection must be sorted and cannot be sorted
private fun List<String>.nearest(string: String, maxDist: Double = Int.MAX_VALUE.toDouble()): Pair<String, Double> {
val idx = binarySearch(string)
return if (idx < 0) {
val l = Levenshtein()
var minSoFar = maxDist
var minIndexSoFar = 0
forEachIndexed { index, s ->
val d = l.distance(string, s, ceil(minSoFar).toInt())
if (d < minSoFar) {
minSoFar = d
minIndexSoFar = index
}
}
get(minIndexSoFar) to minSoFar
} else {
get(idx) to 0.0
}
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response) = parseListing(response)
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/browse/title/date/DESC/$page", headers)
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response) = parseListing(response)
/**
* 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!")
}
override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
val tables = parseIntoTables(input)
with(metadata) {
hbId = Uri.parse(input.location()).pathSegments.first().toLong()
hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
tags.clear()
(tables[""]!! + tables["categories"]!!).forEach { (k, v) ->
((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
when (val lowercaseNs = k.toLowerCase()) {
"title" -> title = v.text()
"length" -> length = v.text().substringBefore(" ").toInt()
@@ -340,35 +67,6 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
}
}
/**
* 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))
}
}
/**
* 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> {
return parseIntoTables(response.asJsoup())["read manga online"]?.map { (key, value) ->
SChapter.create().apply {
url = value.selectFirst(".listLink").attr("href")
name = key
}
} ?: emptyList()
}
private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
return doc.select("#main > .listTable").map { ele ->
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
@@ -378,602 +76,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
}.toMap()
}
/**
* 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 doc = response.asJsoup()
val basePath = listOf("data") + response.request.url.pathSegments
val scripts = doc.getElementsByTag("script").map { it.data() }
for (script in scripts) {
val totalPages = TOTAL_PAGES_REGEX.find(script)?.groupValues?.getOrNull(1)?.toIntOrNull()
?: continue
val pageList = PAGE_LIST_REGEX.find(script)?.groupValues?.getOrNull(1) ?: continue
return JsonParser.parseString(pageList).array.take(totalPages).map {
it.string
}.mapIndexed { index, pageName ->
Page(
index,
pageName,
"$baseUrl/${basePath.joinToString("/")}/$pageName"
)
}
}
return emptyList()
}
class HelpFilter : Filter.HelpDialog(
"Usage instructions",
markdown =
"""
### Modes
There are three available filter modes:
- Text search
- Tag search
- Sort mode
You can only use a single mode at a time. Switch between the text and tag search modes using the dropdown menu. Switch to sorting mode by selecting a sorting option.
### Text search
Search for galleries by title, artist or origin.
### Tag search
Search for galleries by tag (e.g. search for a specific genre, type, setting, etc). Uses nhentai/e-hentai syntax. Refer to the "Search" section on [this page](https://nhentai.net/info/) for more information.
### Sort mode
View a list of all galleries sorted by a specific parameter. Exit sorting mode by resetting the filters using the reset button near the bottom of the screen.
### Tag list
""".trimIndent() + "\n$TAGS_AS_MARKDOWN"
)
class ModeFilter : Filter.Select<String>(
"Mode",
arrayOf(
"Text search",
"Tag search"
)
)
class SortFilter : Filter.Sort("Sort", SORT_OPTIONS.map { it.second }.toTypedArray()) {
companion object {
// internal to display
val SORT_OPTIONS = listOf(
"length" to "Length",
"date" to "Date added",
"rank" to "Rank"
)
}
}
override fun getFilterList() = FilterList(
HelpFilter(),
ModeFilter(),
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(
"www.hbrowse.com",
"hbrowse.com"
)
override fun mapUrlToMangaUrl(uri: Uri): String? {
return "$baseUrl/${uri.pathSegments.first()}"
return "/${uri.pathSegments.first()}/c00001/"
}
companion object {
private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);")
private val TOTAL_PAGES_REGEX = Regex("totalPages *= *([0-9]*);")
private const val BASE_COOKIES = "thumbnails=1;"
private val NS_MAPPINGS = mapOf(
"set" to "setting",
"loc" to "setting",
"location" to "setting",
"fet" to "fetish",
"relation" to "relationship",
"male" to "malebody",
"female" to "femalebody",
"pos" to "position"
)
private val ALL_TAGS = mapOf(
"genre" to listOf(
"action",
"adventure",
"anime",
"bizarre",
"comedy",
"drama",
"fantasy",
"gore",
"historic",
"horror",
"medieval",
"modern",
"myth",
"psychological",
"romance",
"school_life",
"scifi",
"supernatural",
"video_game",
"visual_novel"
),
"type" to listOf(
"anthology",
"bestiality",
"dandere",
"deredere",
"deviant",
"fully_colored",
"furry",
"futanari",
"gender_bender",
"guro",
"harem",
"incest",
"kuudere",
"lolicon",
"long_story",
"netorare",
"non-con",
"partly_colored",
"reverse_harem",
"ryona",
"short_story",
"shotacon",
"transgender",
"tsundere",
"uncensored",
"vanilla",
"yandere",
"yaoi",
"yuri"
),
"setting" to listOf(
"amusement_park",
"attic",
"automobile",
"balcony",
"basement",
"bath",
"beach",
"bedroom",
"cabin",
"castle",
"cave",
"church",
"classroom",
"deck",
"dining_room",
"doctors",
"dojo",
"doorway",
"dream",
"dressing_room",
"dungeon",
"elevator",
"festival",
"gym",
"haunted_building",
"hospital",
"hotel",
"hot_springs",
"kitchen",
"laboratory",
"library",
"living_room",
"locker_room",
"mansion",
"office",
"other",
"outdoor",
"outer_space",
"park",
"pool",
"prison",
"public",
"restaurant",
"restroom",
"roof",
"sauna",
"school",
"school_nurses_office",
"shower",
"shrine",
"storage_room",
"store",
"street",
"teachers_lounge",
"theater",
"tight_space",
"toilet",
"train",
"transit",
"virtual_reality",
"warehouse",
"wilderness"
),
"fetish" to listOf(
"androphobia",
"apron",
"assertive_girl",
"bikini",
"bloomers",
"breast_expansion",
"business_suit",
"chastity_device",
"chinese_dress",
"christmas",
"collar",
"corset",
"cosplay_(female)",
"cosplay_(male)",
"crossdressing_(female)",
"crossdressing_(male)",
"eye_patch",
"food",
"giantess",
"glasses",
"gothic_lolita",
"gyaru",
"gynophobia",
"high_heels",
"hot_pants",
"impregnation",
"kemonomimi",
"kimono",
"knee_high_socks",
"lab_coat",
"latex",
"leotard",
"lingerie",
"maid_outfit",
"mother_and_daughter",
"none",
"nonhuman_girl",
"olfactophilia",
"pregnant",
"rich_girl",
"school_swimsuit",
"shy_girl",
"sisters",
"sleeping_girl",
"sporty",
"stockings",
"strapon",
"student_uniform",
"swimsuit",
"tanned",
"tattoo",
"time_stop",
"twins_(coed)",
"twins_(female)",
"twins_(male)",
"uniform",
"wedding_dress"
),
"role" to listOf(
"alien",
"android",
"angel",
"athlete",
"bride",
"bunnygirl",
"cheerleader",
"delinquent",
"demon",
"doctor",
"dominatrix",
"escort",
"foreigner",
"ghost",
"housewife",
"idol",
"magical_girl",
"maid",
"mamono",
"massagist",
"miko",
"mythical_being",
"neet",
"nekomimi",
"newlywed",
"ninja",
"normal",
"nun",
"nurse",
"office_lady",
"other",
"police",
"priest",
"princess",
"queen",
"school_nurse",
"scientist",
"sorcerer",
"student",
"succubus",
"teacher",
"tomboy",
"tutor",
"waitress",
"warrior",
"witch"
),
"relationship" to listOf(
"acquaintance",
"anothers_daughter",
"anothers_girlfriend",
"anothers_mother",
"anothers_sister",
"anothers_wife",
"aunt",
"babysitter",
"childhood_friend",
"classmate",
"cousin",
"customer",
"daughter",
"daughter-in-law",
"employee",
"employer",
"enemy",
"fiance",
"friend",
"friends_daughter",
"friends_girlfriend",
"friends_mother",
"friends_sister",
"friends_wife",
"girlfriend",
"landlord",
"manager",
"master",
"mother",
"mother-in-law",
"neighbor",
"niece",
"none",
"older_sister",
"patient",
"pet",
"physician",
"relative",
"relatives_friend",
"relatives_girlfriend",
"relatives_wife",
"servant",
"server",
"sister-in-law",
"slave",
"stepdaughter",
"stepmother",
"stepsister",
"stranger",
"student",
"teacher",
"tutee",
"tutor",
"twin",
"underclassman",
"upperclassman",
"wife",
"workmate",
"younger_sister"
),
"maleBody" to listOf(
"adult",
"animal",
"animal_ears",
"bald",
"beard",
"dark_skin",
"elderly",
"exaggerated_penis",
"fat",
"furry",
"goatee",
"hairy",
"half_animal",
"horns",
"large_penis",
"long_hair",
"middle_age",
"monster",
"muscular",
"mustache",
"none",
"short",
"short_hair",
"skinny",
"small_penis",
"tail",
"tall",
"tanned",
"tan_line",
"teenager",
"wings",
"young"
),
"femaleBody" to listOf(
"adult",
"animal_ears",
"bald",
"big_butt",
"chubby",
"dark_skin",
"elderly",
"elf_ears",
"exaggerated_breasts",
"fat",
"furry",
"hairy",
"hair_bun",
"half_animal",
"halo",
"hime_cut",
"horns",
"large_breasts",
"long_hair",
"middle_age",
"monster_girl",
"muscular",
"none",
"pigtails",
"ponytail",
"short",
"short_hair",
"skinny",
"small_breasts",
"tail",
"tall",
"tanned",
"tan_line",
"teenager",
"twintails",
"wings",
"young"
),
"grouping" to listOf(
"foursome_(1_female)",
"foursome_(1_male)",
"foursome_(mixed)",
"foursome_(only_female)",
"one_on_one",
"one_on_one_(2_females)",
"one_on_one_(2_males)",
"orgy_(1_female)",
"orgy_(1_male)",
"orgy_(mainly_female)",
"orgy_(mainly_male)",
"orgy_(mixed)",
"orgy_(only_female)",
"orgy_(only_male)",
"solo_(female)",
"solo_(male)",
"threesome_(1_female)",
"threesome_(1_male)",
"threesome_(only_female)",
"threesome_(only_male)"
),
"scene" to listOf(
"adultery",
"ahegao",
"anal_(female)",
"anal_(male)",
"aphrodisiac",
"armpit_sex",
"asphyxiation",
"blackmail",
"blowjob",
"bondage",
"breast_feeding",
"breast_sucking",
"bukkake",
"cheating_(female)",
"cheating_(male)",
"chikan",
"clothed_sex",
"consensual",
"cunnilingus",
"defloration",
"discipline",
"dominance",
"double_penetration",
"drunk",
"enema",
"exhibitionism",
"facesitting",
"fingering_(female)",
"fingering_(male)",
"fisting",
"footjob",
"grinding",
"groping",
"handjob",
"humiliation",
"hypnosis",
"intercrural",
"interracial_sex",
"interspecies_sex",
"lactation",
"lotion",
"masochism",
"masturbation",
"mind_break",
"nonhuman",
"orgy",
"paizuri",
"phone_sex",
"props",
"rape",
"reverse_rape",
"rimjob",
"sadism",
"scat",
"sex_toys",
"spanking",
"squirt",
"submission",
"sumata",
"swingers",
"tentacles",
"voyeurism",
"watersports",
"x-ray_blowjob",
"x-ray_sex"
),
"position" to listOf(
"69",
"acrobat",
"arch",
"bodyguard",
"butterfly",
"cowgirl",
"dancer",
"deck_chair",
"deep_stick",
"doggy",
"drill",
"ex_sex",
"jockey",
"lap_dance",
"leg_glider",
"lotus",
"mastery",
"missionary",
"none",
"other",
"pile_driver",
"prison_guard",
"reverse_piggyback",
"rodeo",
"spoons",
"standing",
"teaspoons",
"unusual",
"victory"
)
).mapValues { it.value.sorted() }
private val TAGS_AS_MARKDOWN = ALL_TAGS.map { (ns, values) ->
"#### $ns\n" + values.map { "- $it" }.joinToString("\n")
}.joinToString("\n\n")
override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
return HBrowseDescriptionAdapter(controller)
}
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
@@ -8,18 +9,20 @@ 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.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.HentaiCafeSearchMetadata
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document
import rx.Observable
class HentaiCafe(delegate: HttpSource) :
class HentaiCafe(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource {
@@ -34,7 +37,7 @@ class HentaiCafe(delegate: HttpSource) :
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
@@ -110,4 +113,8 @@ class HentaiCafe(delegate: HttpSource) :
"https://hentai.cafe/$lcFirstPathSegment"
}
}
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
return HentaiCafeDescriptionAdapter(controller)
}
}
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
@@ -8,17 +9,20 @@ 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.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.PururinSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.PururinDescriptionAdapter
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document
import rx.Observable
class Pururin(delegate: HttpSource) :
class Pururin(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<PururinSearchMetadata, Document>,
UrlImportableSource {
@@ -38,7 +42,7 @@ class Pururin(delegate: HttpSource) :
"$baseUrl/gallery/$trimmedIdQuery/-"
} else query
return urlImportFetchSearchManga(newQuery) {
return urlImportFetchSearchManga(context, newQuery) {
super.fetchSearchManga(page, query, filters)
}
}
@@ -88,10 +92,11 @@ class Pururin(delegate: HttpSource) :
else -> {
value.select("a").forEach { link ->
val searchUrl = Uri.parse(link.attr("href"))
val namespace = searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2]
tags += RaisedTag(
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2],
namespace,
searchUrl.lastPathSegment!!.substringBefore("."),
PururinSearchMetadata.TAG_TYPE_DEFAULT
if (namespace != PururinSearchMetadata.TAG_NAMESPACE_CATEGORY) PururinSearchMetadata.TAG_TYPE_DEFAULT else TAG_TYPE_VIRTUAL
)
}
}
@@ -108,4 +113,8 @@ class Pururin(delegate: HttpSource) :
override fun mapUrlToMangaUrl(uri: Uri): String? {
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}"
}
override fun getDescriptionAdapter(controller: MangaController): PururinDescriptionAdapter {
return PururinDescriptionAdapter(controller)
}
}
@@ -1,25 +1,31 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.TsuminoSearchMetadata
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Document
import rx.Observable
class Tsumino(delegate: HttpSource) :
class Tsumino(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>,
UrlImportableSource {
@@ -27,13 +33,13 @@ class Tsumino(delegate: HttpSource) :
override val lang = "en"
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) {
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase(Locale.ROOT) ?: return null
if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
return null
}
@@ -57,9 +63,12 @@ class Tsumino(delegate: HttpSource) :
title = it.trim()
}
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let {
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL))
artist = it
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { artistString ->
artistString.split("|").trimAll().dropBlank().forEach {
tags.add(RaisedTag("artist", it, TAG_TYPE_DEFAULT))
}
tags.add(RaisedTag("artist", artistString, TAG_TYPE_VIRTUAL))
artist = artistString
}
input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
@@ -76,6 +85,12 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Rating")?.text()?.let {
ratingString = it.trim()
val ratingString = ratingString
if (!ratingString.isNullOrBlank()) {
averageRating = RATING_FLOAT_REGEX.find(ratingString)?.groups?.get(1)?.value?.toFloatOrNull()
userRatings = RATING_USERS_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
favorites = RATING_FAVORITES_REGEX.find(ratingString)?.groups?.get(1)?.value?.toLongOrNull()
}
}
input.getElementById("Category")?.children()?.first()?.text()?.let {
@@ -85,18 +100,19 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Collection")?.children()?.first()?.text()?.let {
collection = it.trim()
tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT))
}
input.getElementById("Group")?.children()?.first()?.text()?.let {
group = it.trim()
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL))
tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT))
}
val newParody = mutableListOf<String>()
input.getElementById("Parody")?.children()?.forEach {
val entry = it.text().trim()
newParody.add(entry)
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL))
tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT))
}
parody = newParody
@@ -104,14 +120,14 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Character")?.children()?.forEach {
val entry = it.text().trim()
newCharacter.add(entry)
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL))
tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT))
}
character = newCharacter
input.getElementById("Tag")?.children()?.let {
input.getElementById("Tag")?.children()?.let { tagElements ->
tags.addAll(
it.map {
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT)
tagElements.map {
RaisedTag("tags", it.text().trim(), TAG_TYPE_DEFAULT)
}
)
}
@@ -125,6 +141,12 @@ class Tsumino(delegate: HttpSource) :
companion object {
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US)
private val ASP_NET_COOKIE_NAME = "ASP.NET_SessionId"
val RATING_FLOAT_REGEX = "([0-9].*) \\(".toRegex()
val RATING_USERS_REGEX = "\\(([0-9].*) users".toRegex()
val RATING_FAVORITES_REGEX = "/ ([0-9].*) favs".toRegex()
}
override fun getDescriptionAdapter(controller: MangaController): TsuminoDescriptionAdapter {
return TsuminoDescriptionAdapter(controller)
}
}
@@ -74,7 +74,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
return null
}
fun setTitle() {
fun setTitle(title: String? = null) {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) {
@@ -83,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
parentController = parentController.parentController
}
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
(activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
}
private fun Controller.instance(): String {
@@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
}
}
fun Controller.withFadeTransaction(): RouterTransaction {
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
.pushChangeHandler(FadeChangeHandler(duration))
.popChangeHandler(FadeChangeHandler(duration))
}
@@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
/**
* Dismiss the dialog and pop this controller
*/
fun dismissDialog() {
private fun dismissDialog() {
if (dismissed) {
return
}
@@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.ui.base.controller
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
interface FabController {
fun configureFab(fab: ExtendedFloatingActionButton) {}
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
}
@@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.ui.base.controller
interface ToolbarLiftOnScrollController
@@ -88,7 +88,7 @@ class BrowseController :
override fun configureTabs(tabs: TabLayout) {
with(tabs) {
tabGravity = TabLayout.GRAVITY_FILL
tabMode = TabLayout.MODE_AUTO
tabMode = TabLayout.MODE_FIXED
}
}
@@ -1,10 +1,11 @@
package eu.kanade.tachiyomi.ui.browse.source
package eu.kanade.tachiyomi.ui.browse
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.marginBottom
import androidx.recyclerview.widget.RecyclerView
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
@@ -22,11 +23,10 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child)
if (holder is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder
if (holder is SourceListItem &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val top = child.bottom + child.marginBottom
val bottom = top + divider.intrinsicHeight
val left = parent.paddingStart + holder.margin
val right = parent.width - parent.paddingEnd - holder.margin
@@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.ui.browse
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
interface SourceListItem : SlicedHolder
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@@ -75,7 +76,7 @@ open class ExtensionController :
// Create recycler and set adapter.
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
adapter?.fastScroller = binding.fastScroller
}
@@ -129,6 +130,9 @@ open class ExtensionController :
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
@@ -142,9 +146,6 @@ open class ExtensionController :
drawExtensions()
}
.launchIn(scope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
}
override fun onItemClick(view: View, position: Int): Boolean {
@@ -1,48 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.extension
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable
init {
val a = context.obtainStyledAttributes(intArrayOf(android.R.attr.listDivider))
divider = a.getDrawable(0)!!
a.recycle()
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val childCount = parent.childCount
for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child)
if (holder is ExtensionHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
) {
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight
val left = parent.paddingStart + holder.margin
val right = parent.width - parent.paddingEnd - holder.margin
divider.setBounds(left, top, right, bottom)
divider.draw(c)
}
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.set(0, 0, 0, divider.intrinsicHeight)
}
}
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.ui.browse.SourceListItem
import eu.kanade.tachiyomi.util.system.LocaleHelper
import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.extension_card_item.card
@@ -22,6 +23,7 @@ import uy.kohesive.injekt.api.get
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter),
SourceListItem,
SlicedHolder {
override val slice = Slice(card).apply {
@@ -45,12 +47,15 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
version.text = extension.versionName
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
warning.text = when {
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase()
else -> null
}
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
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)
// SY -->
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant)
// SY <--
extension.isNsfw -> itemView.context.getString(R.string.ext_nsfw_short)
else -> ""
}.toUpperCase()
GlideApp.with(itemView.context).clear(image)
if (extension is Extension.Available) {
@@ -91,12 +96,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
setText(R.string.ext_update)
}
else -> {
// SY -->
if (extension.sources.any { it is ConfigurableSource }) {
@SuppressLint("SetTextI18n")
text = context.getString(R.string.action_settings) + "+"
} else {
setText(R.string.action_settings)
}
// SY <--
}
}
} else if (extension is Extension.Untrusted) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import android.os.Bundle
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
val (installed, untrusted, available) = tuple
val items = mutableListOf<ExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName }
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { 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 availableSorted = available
// Filter out already installed extensions and disabled languages
.filter { avail ->
installed.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 }
@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
@@ -23,14 +26,16 @@ import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
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.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange
@@ -45,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
NoToolbarElevationController {
ToolbarLiftOnScrollController {
private val preferences: PreferencesHelper by injectLazy()
@@ -180,6 +185,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
when (item.itemId) {
R.id.action_enable_all -> toggleAllSources(true)
R.id.action_disable_all -> toggleAllSources(false)
R.id.action_open_in_settings -> openInSettings()
}
return super.onOptionsItemSelected(item)
}
@@ -193,15 +199,18 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
}
private fun toggleSource(source: Source, enable: Boolean) {
val current = preferences.disabledSources().get()
if (enable) {
preferences.disabledSources() -= source.id.toString()
} else {
preferences.disabledSources() += source.id.toString()
}
}
preferences.disabledSources().set(
if (enable) {
current - source.id.toString()
} else {
current + source.id.toString()
}
)
private fun openInSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", presenter.pkgName, null)
}
startActivity(intent)
}
private fun Source.isEnabled(): Boolean {
@@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
binding.extensionTitle.text = extension.name
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.extensionNsfw.isVisible = extension.isNsfw
binding.extensionPkg.text = extension.pkgName
binding.extensionUninstallButton.clicks()
@@ -49,18 +50,18 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
.launchIn(scope)
if (extension.isObsolete) {
binding.extensionWarningBanner.visible()
binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
}
if (extension.isUnofficial) {
binding.extensionWarningBanner.visible()
binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
}
// SY -->
if (extension.isRedundant) {
binding.extensionWarningBanner.visible()
binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
}
// SY <--
@@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.launchIn
@@ -72,11 +71,7 @@ open class LatestController :
*/
override fun onMangaClick(manga: Manga) {
// Open MangaController.
if (presenter.preferences.eh_useNewMangaInterface().get()) {
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
} else {
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
}
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
}
/**
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.latest
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.android.synthetic.main.latest_controller_card.no_results_found
import kotlinx.android.synthetic.main.latest_controller_card.progress
import kotlinx.android.synthetic.main.latest_controller_card.recycler
import kotlinx.android.synthetic.main.latest_controller_card.source_card
@@ -61,16 +61,16 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
when {
results == null -> {
progress.visible()
showHolder()
progress.isVisible = true
showResultsHolder()
}
results.isEmpty() -> {
progress.gone()
hideHolder()
progress.isVisible = false
showNoResults()
}
else -> {
progress.gone()
showHolder()
progress.isVisible = false
showResultsHolder()
}
}
if (results !== lastBoundResults) {
@@ -105,13 +105,13 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
return null
}
private fun showHolder() {
title_wrapper.visible()
source_card.visible()
private fun showResultsHolder() {
no_results_found.isVisible = false
source_card.isVisible = true
}
private fun hideHolder() {
title_wrapper.gone()
source_card.gone()
private fun showNoResults() {
no_results_found.isVisible = true
source_card.isVisible = false
}
}

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