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 * Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs # Bugs
* Include version (Setting > About > Version) * Include version (More > About > Version)
* If not latest, try updating, it may have already been solved * 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 steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible) * 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 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 - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -10,7 +10,7 @@ I acknowledge that:
--- ---
### Device information ## Device information
* Tachiyomi version: ? * Tachiyomi version: ?
* Android version: ? * Android version: ?
* Device: ? * Device: ?
+3 -3
View File
@@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2) - I have updated to the latest version of the app (stable is v1.2.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -17,7 +17,7 @@ I acknowledge that:
--- ---
### Device information ## Device information
* Tachiyomi version: ? * Tachiyomi version: ?
* Android version: ? * Android version: ?
* Device: ? * Device: ?
@@ -32,5 +32,5 @@ This should happen.
### Actual behavior ### Actual behavior
This happened instead. This happened instead.
### Other details ## Other details
Additional details and attachments. Additional details and attachments.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Tachiyomi help website
url: https://tachiyomi.org/help/
about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository
url: https://github.com/inorichi/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead.
+3 -3
View File
@@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2) - I have updated to the latest version of the app (stable is v1.2.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@@ -17,8 +17,8 @@ I acknowledge that:
--- ---
### Why/User Benefit/User Problem ## Why/User Benefit/User Problem
(explain why this feature should be added) (explain why this feature should be added)
### What/Requirements ## What/Requirements
(explain how this feature would behave) (explain how this feature would behave)
+2 -2
View File
@@ -2,7 +2,7 @@
name: "Extension/source/catalogue issue" name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions" about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions" title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
labels: "catalog" labels: "catalog, invalid"
--- ---
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

After

Width:  |  Height:  |  Size: 482 KiB

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

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -2,6 +2,8 @@ name: Remote Dispatch Action Initiator
on: on:
push: push:
branches:
- 'master'
repository_dispatch: repository_dispatch:
jobs: jobs:
@@ -0,0 +1,67 @@
name: Release Builder
on:
push:
branches:
- 'release'
jobs:
apk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Get NDK
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Write google-services.json
uses: DamianReeves/write-file-action@v1.0
with:
# The path to the file to write
path: app/google-services.json
# The contents of the file
contents: ${{ secrets.GOOGLE_SERVICES_TEXT }}
# The mode of writing to use: `overwrite`, `append`, or `preserve`.
write-mode: overwrite # optional, default is preserve
- name: Build Release APK
run: bash ./gradlew assembleRelease --stacktrace
- name: Sign Android Release
uses: r0adkll/sign-android-release@v1
with:
# The directory to find your release to sign
releaseDirectory: app/build/outputs/apk/standard/release
# The key used to sign your release in base64 encoded format
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
# The key alias
alias: ${{ secrets.ALIAS }}
# The password to the keystore
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
# The password for the key
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.run_number }}
release_name: TachiyomiSY
draft: true
prerelease: false
- name: Upload Release APK
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
asset_name: TachiyomiSY.apk
asset_content_type: application/vnd.android.package-archive
+18 -10
View File
@@ -1,6 +1,6 @@
| Preview Builds | Release Builds | Tachiyomi Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) | | [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
@@ -22,7 +22,6 @@ Features of Tachiyomi(original) include:
Features of TachiyomiSY include: Features of TachiyomiSY include:
* Uses the new Tachiyomi Stable UI * Uses the new Tachiyomi Stable UI
* Custom manga page, all your needs, such as info and chapters, in front of your face
* Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab * Latest tab, store up to 5 sources where you can easily view the latest manga by viewing the tab
* Hentai features enable/disable, in advanced settings * Hentai features enable/disable, in advanced settings
* Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one * Automatic webtoon detection, allowing the reader to switch to webtoon mode automatically when viewing one
@@ -34,20 +33,28 @@ Features of TachiyomiSY include:
* New E-Hentai/ExHentai features, such as language settings and watched list settings * New E-Hentai/ExHentai features, such as language settings and watched list settings
* Comfortable grid view * Comfortable grid view
* Custom categories for sources, liked the pinned sources, but you can make your own versions and put any sources in them * 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 Inherited from TachiyomiAZ or TachiyomiEH and are included and possibly modified in TachiyomiSY
* Source migration, migrate all your manga from one source to another * Source migration, migrate all your manga from one source to another
* Custom hentai sources: * Custom hentai sources:
* * E-Hentai/ExHentai * * E-Hentai/ExHentai
* * nHentai
* * Hitomi.la
* * 8Muses
* * HBrowse
* * Perv Eden
* Additional features for some extensions, features include custom description, opening in app, batch add to library: * Additional features for some extensions, features include custom description, opening in app, batch add to library:
* * 8Muses (EroMuse)
* * HBrowse
* * HentaiCafe (Foolside)
* * Hitomi.la
* * NHentai
* * PervEden (EN and IT)
* * Puruin * * Puruin
* * Tsumino * * Tsumino
* * HentaiCafe (Foolside)
* Saving searches * Saving searches
* Autoscroll * Autoscroll
* Page preload customization * 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 * Click tag for local search, long click tag for global search
* Merge multiple of the same manga from different sources * Merge multiple of the same manga from different sources
* Drag and drop library sorting * Drag and drop library sorting
* Library search engine, includes exclude, quotes as absolute, and a bunch of other ways to search
## Download ## Download
Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases). Get the app from our [releases page](https://github.com/jobobby04/tachiyomisy/releases/latest).
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases). If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/jobobby04/tachiyomisypreview/releases).
@@ -81,7 +89,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Bugs</summary> <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 * 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 * 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 steps to reproduce (if not obvious from description)
+34 -28
View File
@@ -36,15 +36,14 @@ ext {
android { android {
compileSdkVersion AndroidConfig.compileSdk compileSdkVersion AndroidConfig.compileSdk
buildToolsVersion AndroidConfig.buildTools buildToolsVersion AndroidConfig.buildTools
publishNonDefault true
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi.sy" applicationId "eu.kanade.tachiyomi.sy"
minSdkVersion AndroidConfig.minSdk minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 2 versionCode 6
versionName "1.0.0" versionName "1.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -129,7 +128,7 @@ android {
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
} }
@@ -139,38 +138,34 @@ androidExtensions {
dependencies { dependencies {
// Modified dependencies
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// AndroidX libraries // AndroidX libraries
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' 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.browser:browser:1.2.0'
implementation 'androidx.cardview:cardview:1.0.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.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.core:core-ktx:1.4.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'androidx.webkit:webkit:1.3.0-rc01'
final lifecycle_version = '2.3.0-alpha05' final lifecycle_version = '2.3.0-alpha06'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Job scheduling // Job scheduling
final work_version = '2.4.0-rc01' final work_version = '2.4.0'
implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version"
// UI library // 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 // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
@@ -179,11 +174,11 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0' implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client // Network client
final okhttp_version = '4.7.2' final okhttp_version = '4.8.1'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version" implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
implementation 'com.squareup.okio:okio:2.6.0' implementation 'com.squareup.okio:okio:2.7.0'
// TLS 1.3 support for Android < 10 // TLS 1.3 support for Android < 10
implementation 'org.conscrypt:conscrypt-android:2.4.0' implementation 'org.conscrypt:conscrypt-android:2.4.0'
@@ -204,6 +199,7 @@ dependencies {
// Disk // Disk
implementation 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588' implementation 'com.github.inorichi:unifile:e9ee588'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// HTML parser // HTML parser
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
@@ -218,10 +214,10 @@ dependencies {
implementation 'androidx.sqlite:sqlite:2.1.0' implementation 'androidx.sqlite:sqlite:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.31.0' implementation 'io.requery:sqlite-android:3.32.2'
// Preferences // Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1' implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0'
// Model View Presenter // Model View Presenter
final nucleus_version = '3.0.0' final nucleus_version = '3.0.0'
@@ -237,12 +233,13 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
// Logging // Logging
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports // Crash reports
final acra_version = '5.5.0' //implementation 'ch.acra:acra-http:5.7.0'
implementation "ch.acra:acra-http:$acra_version"
// Sort // Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
@@ -272,7 +269,7 @@ dependencies {
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1' implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
// FlowBinding // 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-android:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
@@ -280,7 +277,7 @@ dependencies {
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Licenses // Licenses
final aboutlibraries_version = '8.2.0' final aboutlibraries_version = '8.3.0'
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version" implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version" implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
@@ -297,14 +294,20 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$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-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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-reactive:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$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) // Text distance (EH)
implementation 'info.debatty:java-string-similarity:1.2.1' implementation 'info.debatty:java-string-similarity:1.2.1'
@@ -338,6 +341,9 @@ dependencies {
// Humanize (EH) // Humanize (EH)
implementation 'com.github.mfornos:humanize-slim:1.2.2' 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' implementation 'androidx.gridlayout:gridlayout:1.0.0'
final def markwon_version = '4.1.0' final def markwon_version = '4.1.0'
@@ -380,7 +386,7 @@ task copyResources(type: Copy) {
preBuild.dependsOn(ktlintFormat, copyResources) 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' apply plugin: 'com.google.gms.google-services'
// Firebase (EH) // Firebase (EH)
apply plugin: 'io.fabric' apply plugin: 'io.fabric'
+8
View File
@@ -37,6 +37,14 @@
public *; public *;
} }
# Hitomi extension crash fix
-keepclassmembers class rx.Single {
*** onSubscribe;
final *;
protected *;
public *;
}
# RxJava 1.1.0 # RxJava 1.1.0
-dontwarn sun.misc.** -dontwarn sun.misc.**
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

-5
View File
@@ -39,11 +39,6 @@
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<activity
android:name=".ui.main.ForceCloseActivity"
android:clearTaskOnLaunch="true"
android:noHistory="true"
android:theme="@android:style/Theme.NoDisplay" />
<activity <activity
android:name=".ui.main.DeepLinkActivity" android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
+9 -10
View File
@@ -6,7 +6,6 @@ import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.widget.Toast
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
@@ -29,11 +28,8 @@ import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule import com.ms_square.debugoverlay.modules.FpsModule
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.ForceCloseActivity
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper 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.debug.DebugToggles
import exh.log.CrashlyticsPrinter import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
@@ -63,11 +59,14 @@ open class App : Application(), LifecycleObserver {
workaroundAndroid7BrokenSSL() workaroundAndroid7BrokenSSL()
// Enforce WebView availability // Debug tool; see https://fbflipper.com/
if (!WebViewUtil.supportsWebView(this)) { // SoLoader.init(this, false)
toast(R.string.information_webview_required, Toast.LENGTH_LONG) // if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
ForceCloseActivity.closeApp(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 // TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@@ -78,6 +77,7 @@ open class App : Application(), LifecycleObserver {
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
setupNotificationChannels() setupNotificationChannels()
Realm.init(this)
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH) GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
// Reprint.initialize(this) //Setup fingerprint (EH) // Reprint.initialize(this) //Setup fingerprint (EH)
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
@@ -134,7 +134,6 @@ open class App : Application(), LifecycleObserver {
// EXH // EXH
private fun deleteOldMetadataRealm() { private fun deleteOldMetadataRealm() {
Realm.init(this)
val config = RealmConfiguration.Builder() val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm") .name("gallery-metadata.realm")
.schemaVersion(3) .schemaVersion(3)
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
@@ -42,6 +43,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadManager(app) }
addSingletonFactory { CustomMangaManager(app) }
addSingletonFactory { TrackManager(app) } addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() } addSingletonFactory { Gson() }
@@ -63,5 +66,9 @@ class AppModule(val app: Application) : InjektModule {
GlobalScope.launch { get<DatabaseHelper>() } GlobalScope.launch { get<DatabaseHelper>() }
GlobalScope.launch { get<DownloadManager>() } GlobalScope.launch { get<DownloadManager>() }
// SY -->
GlobalScope.launch { get<CustomMangaManager>() }
// SY <--
} }
} }
@@ -87,6 +87,7 @@ object Migrations {
} }
if (oldVersion < 44) { if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source // Reset sorting preference if using removed sort by source
@Suppress("DEPRECATION")
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) { if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
preferences.librarySortingMode().set(LibrarySort.ALPHA) 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.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
@@ -106,7 +107,7 @@ class BackupCreateService : Service() {
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0) val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
backupManager = BackupManager(this) 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) val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile) notifier.showBackupComplete(unifile)
} catch (e: Exception) { } catch (e: Exception) {
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
@@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context) val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().get()) val uri = preferences.backupsDirectory().get().toUri()
val flags = BackupCreateService.BACKUP_ALL val flags = BackupCreateService.BACKUP_ALL
return try { return try {
backupManager.createBackup(uri, flags, true) backupManager.createBackup(uri, flags, true)
@@ -359,7 +359,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
for (dbCategory in dbCategories) { for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category // If the category is already in the db, assign the id to the file's category
// and do nothing // and do nothing
if (category.nameLower == dbCategory.nameLower) { if (category.name == dbCategory.name) {
category.id = dbCategory.id category.id = dbCategory.id
found = true found = true
break break
@@ -387,7 +387,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>() val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
for (backupCategoryStr in categories) { for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) { for (dbCategory in dbCategories) {
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) { if (backupCategoryStr == dbCategory.name) {
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory)) mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
break break
} }
@@ -105,6 +105,10 @@ class BackupRestoreService : Service() {
// SY --> // SY -->
private val throttleManager = EHentaiThrottleManager() private val throttleManager = EHentaiThrottleManager()
private var skippedAmount = 0
private var totalAmount = 0
// SY <-- // SY <--
/** /**
@@ -117,12 +121,6 @@ class BackupRestoreService : Service() {
*/ */
private var restoreAmount = 0 private var restoreAmount = 0
// SY -->
private var skippedAmount = 0
private var totalAmount = 0
// SY <--
/** /**
* Mapping of source ID to source name from backup data * Mapping of source ID to source name from backup data
*/ */
@@ -288,7 +286,7 @@ class BackupRestoreService : Service() {
backupManager.restoreSavedSearches(savedSearchesJson) backupManager.restoreSavedSearches(savedSearchesJson)
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.eh_saved_searches)) showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
} }
// SY <-- // SY <--
@@ -320,13 +318,8 @@ class BackupRestoreService : Service() {
if (source != null) { if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks) restoreMangaData(manga, source, chapters, categories, history, tracks)
} else { } else {
val message = if (manga.source in sourceMapping) { val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
getString(R.string.source_not_found_name, sourceMapping[manga.source]) errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
} else {
getString(R.string.source_not_found)
}
errors.add(Date() to "${manga.title} - $message")
} }
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}") errors.add(Date() to "${manga.title} - ${e.message}")
@@ -7,16 +7,22 @@ import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.injectLazy
object BackupRestoreValidator { object BackupRestoreValidator {
private val sourceManager: SourceManager by injectLazy()
private val trackManager: TrackManager by injectLazy()
/** /**
* Checks for critical backup file data. * Checks for critical backup file data.
* *
* @throws Exception if version or manga cannot be found. * @throws Exception if version or manga cannot be found.
* @return List of required sources. * @return List of missing sources or missing trackers.
*/ */
fun validate(context: Context, uri: Uri): Map<Long, String> { fun validate(context: Context, uri: Uri): Results {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject val json = JsonParser.parseReader(reader).asJsonObject
@@ -26,11 +32,29 @@ object BackupRestoreValidator {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data)) throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
} }
if (mangasJson.asJsonArray.size() == 0) { val mangas = mangasJson.asJsonArray
if (mangas.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
} }
return getSourceMapping(json) val sources = getSourceMapping(json)
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.values
.sorted()
val trackers = mangas
.filter { it.asJsonObject.has("track") }
.flatMap { it.asJsonObject["track"].asJsonArray }
.map { it.asJsonObject["s"].asInt }
.distinct()
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.sorted()
return Results(missingSources, missingTrackers)
} }
fun getSourceMapping(json: JsonObject): Map<Long, String> { fun getSourceMapping(json: JsonObject): Map<Long, String> {
@@ -43,4 +67,6 @@ object BackupRestoreValidator {
} }
.toMap() .toMap()
} }
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
} }
@@ -14,7 +14,9 @@ object MangaTypeAdapter {
write { write {
beginArray() beginArray()
value(it.url) value(it.url)
value(it.title) // SY -->
value(it.originalTitle)
// SY <--
value(it.source) value(it.source)
value(it.viewer) value(it.viewer)
value(it.chapter_flags) value(it.chapter_flags)
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * 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) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -66,6 +66,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 2) { if (oldVersion < 2) {
db.execSQL(MangaTable.addCoverLastModified) db.execSQL(MangaTable.addCoverLastModified)
} }
if (oldVersion < 3) {
db.execSQL(MangaTable.addDateAdded)
db.execSQL(MangaTable.backfillDateAdded)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { 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_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS 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_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_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
@@ -47,15 +48,17 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
put(COL_SOURCE, obj.source) put(COL_SOURCE, obj.source)
put(COL_URL, obj.url) put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist) // SY -->
put(COL_AUTHOR, obj.author) put(COL_ARTIST, obj.originalArtist)
put(COL_DESCRIPTION, obj.description) put(COL_AUTHOR, obj.originalAuthor)
put(COL_GENRE, obj.genre) put(COL_DESCRIPTION, obj.originalDescription)
put(COL_TITLE, obj.title) put(COL_GENRE, obj.originalGenre)
put(COL_TITLE, obj.originalTitle)
// SY <--
put(COL_STATUS, obj.status) put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url) put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite) put(COL_FAVORITE, obj.favorite)
@@ -64,6 +67,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
put(COL_VIEWER, obj.viewer) put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags) put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified) 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)) viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)) 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> var mangaOrder: List<Long>
// SY <-- // SY <--
val nameLower: String
get() = name.toLowerCase()
companion object { companion object {
fun create(name: String): Category = CategoryImpl().apply { fun create(name: String): Category = CategoryImpl().apply {
@@ -19,7 +19,6 @@ class CategoryImpl : Category {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val category = other as Category val category = other as Category
return name == category.name return name == category.name
} }
@@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter val chapter = other as Chapter
return url == chapter.url if (url != chapter.url) return false
return id == chapter.id
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode() + id.hashCode()
} }
} }
@@ -12,6 +12,8 @@ interface Manga : SManga {
var last_update: Long var last_update: Long
var date_added: Long
var viewer: Int var viewer: Int
var chapter_flags: Int var chapter_flags: Int
@@ -22,10 +24,6 @@ interface Manga : SManga {
setFlags(order, SORT_MASK) 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 { fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC return chapter_flags and SORT_MASK == SORT_DESC
} }
@@ -34,6 +32,16 @@ interface Manga : SManga {
return genre?.split(", ")?.map { it.trim() } return genre?.split(", ")?.map { it.trim() }
} }
// SY -->
fun getOriginalGenres(): List<String>? {
return originalGenre?.split(", ")?.map { it.trim() }
}
// SY <--
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
// Used to display the chapter's title one way or another // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and DISPLAY_MASK get() = chapter_flags and DISPLAY_MASK
@@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.database.models package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import uy.kohesive.injekt.injectLazy
open class MangaImpl : Manga { open class MangaImpl : Manga {
override var id: Long? = null override var id: Long? = null
@@ -9,17 +12,36 @@ open class MangaImpl : Manga {
override lateinit var url: String override lateinit var url: String
// SY --> // 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 <-- // 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 status: Int = 0
override var thumbnail_url: String? = null override var thumbnail_url: String? = null
@@ -28,6 +50,8 @@ open class MangaImpl : Manga {
override var last_update: Long = 0 override var last_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false override var initialized: Boolean = false
override var viewer: Int = 0 override var viewer: Int = 0
@@ -36,16 +60,29 @@ open class MangaImpl : Manga {
override var cover_last_modified: Long = 0 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga val manga = other as Manga
if (url != manga.url) return false
return url == manga.url return id == manga.id
} }
override fun hashCode(): Int { 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.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver 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.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
@@ -84,6 +85,16 @@ interface MangaQueries : DbProvider {
.build() .build()
) )
.prepare() .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 <-- // SY <--
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() 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} 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} 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 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_LAST_UPDATE = "last_update"
const val COL_DATE_ADDED = "date_added"
const val COL_INITIALIZED = "initialized" const val COL_INITIALIZED = "initialized"
const val COL_VIEWER = "viewer" const val COL_VIEWER = "viewer"
@@ -58,7 +60,8 @@ object MangaTable {
$COL_INITIALIZED BOOLEAN NOT NULL, $COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL, $COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS 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 val createUrlIndexQuery: String
@@ -70,4 +73,17 @@ object MangaTable {
val addCoverLastModified: String val addCoverLastModified: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0" 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 package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -59,7 +59,7 @@ class DownloadCache(
*/ */
private fun getDirectoryFromPreference(): UniFile { private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().get() 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) { if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) { if (mangaDir != null) {
return provider.getChapterDirName(chapter) in mangaDir.files return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
} }
} }
return false return false
@@ -122,7 +122,9 @@ class DownloadCache(
* Renews the downloads cache. * Renews the downloads cache.
*/ */
private fun renew() { private fun renew() {
val onlineSources = sourceManager.getOnlineSources() // SY -->
val onlineSources = sourceManager.getVisibleOnlineSources()
// SY <--
val sourceDirs = rootDir.dir.listFiles() val sourceDirs = rootDir.dir.listFiles()
.orEmpty() .orEmpty()
@@ -191,12 +193,25 @@ class DownloadCache(
fun removeChapter(chapter: Chapter, manga: Manga) { fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
val chapterDirName = provider.getChapterDirName(chapter) provider.getValidChapterDirNames(chapter).forEach {
if (chapterDirName in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= chapterDirName 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. * 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 sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
chapters.forEach { chapter -> chapters.forEach { chapter ->
val chapterDirName = provider.getChapterDirName(chapter) provider.getValidChapterDirNames(chapter).forEach {
if (chapterDirName in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= chapterDirName 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.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -22,12 +23,10 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
*/ */
class DownloadManager(private val context: Context) { class DownloadManager(/* SY private */ val context: Context) {
/** private val sourceManager: SourceManager by injectLazy()
* The sources manager. private val preferences: PreferencesHelper by injectLazy()
*/
private val sourceManager by injectLazy<SourceManager>()
/** /**
* Downloads provider, used to retrieve the folders where the chapters are or should be stored. * Downloads provider, used to retrieve the folders where the chapters are or should be stored.
@@ -201,14 +200,47 @@ class DownloadManager(private val context: Context) {
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
queue.remove(chapters) queue.remove(chapters)
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
val filteredChapters = if (!preferences.removeBookmarkedChapters()) {
chapters.filterNot { it.bookmark }
} else {
chapters
}
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() } chapterDirs.forEach { it.delete() }
cache.removeChapters(chapters, manga) cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete() chapterDirs.firstOrNull()?.parentFile?.delete()
} }
} }
// SY -->
/**
* Deletes the directories of chapters that were read or have no match
*
* @param chapters the list of chapters to delete.
* @param manga the manga of the chapters.
* @param source the source of the chapters.
*/
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int {
var cleaned = 0
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
cleaned += filesWithNoChapter.size
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
filesWithNoChapter.forEach { it.delete() }
val readChapters = allChapters.filter { it.read }
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
readChapterDirs.forEach { it.delete() }
cleaned += readChapterDirs.size
cache.removeChapters(readChapters, manga)
if (cache.getDownloadCount(manga) == 0) {
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty
}
return cleaned
}
// SY <--
/** /**
* Deletes the directory of a downloaded manga. * Deletes the directory of a downloaded manga.
* *
@@ -251,16 +283,20 @@ class DownloadManager(private val context: Context) {
* @param newChapter the target chapter with the new name. * @param newChapter the target chapter with the new name.
*/ */
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) { 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 newName = provider.getChapterDirName(newChapter)
val mangaDir = provider.getMangaDir(manga, source) 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) { if (oldFolder?.renameTo(newName) == true) {
cache.removeChapter(oldChapter, manga) cache.removeChapter(oldChapter, manga)
cache.addChapter(newName, mangaDir, manga) cache.addChapter(newName, mangaDir, manga)
} else { } 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.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.regex.Pattern import java.util.regex.Pattern
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
/** /**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters. * 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) { internal class DownloadNotifier(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) { private val preferences: PreferencesHelper by injectLazy()
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
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. * Status of download. Used for correct notification icon.
*/ */
@Volatile
private var isDownloading = false private var isDownloading = false
/** /**
@@ -50,14 +62,14 @@ internal class DownloadNotifier(private val context: Context) {
* *
* @param id the id of the notification. * @param id the id of the notification.
*/ */
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) { private fun NotificationCompat.Builder.show(id: Int) {
context.notificationManager.notify(id, build()) context.notificationManager.notify(id, build())
} }
/** /**
* Clear old actions if they exist. * Clear old actions if they exist.
*/ */
private fun clearActions() = with(notificationBuilder) { private fun NotificationCompat.Builder.clearActions() {
if (mActions.isNotEmpty()) { if (mActions.isNotEmpty()) {
mActions.clear() 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 * Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user. * those can only be dismissed by the user.
*/ */
fun dismiss() { fun dismissProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER) 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. * @param download download object containing download information.
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
// Create notification with(progressNotificationBuilder) {
with(notificationBuilder) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) 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) setProgress(download.pages!!.size, download.downloadedImages, false)
}
// Displays the progress bar on notification show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
notificationBuilder.show() }
} }
/** /**
* Show notification when download is paused. * Show notification when download is paused.
*/ */
fun onDownloadPaused() { fun onPaused() {
with(notificationBuilder) { with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.chapter_paused)) setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
@@ -141,22 +151,45 @@ internal class DownloadNotifier(private val context: Context) {
context.getString(R.string.action_cancel_all), context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context) NotificationReceiver.clearDownloadsPendingBroadcast(context)
) )
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
} }
// Show notification.
notificationBuilder.show()
// Reset initial values // Reset initial values
isDownloading = false 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. * Called when the downloader receives a warning.
* *
* @param reason the text to show. * @param reason the text to show.
*/ */
fun onWarning(reason: String) { fun onWarning(reason: String) {
with(notificationBuilder) { with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason) setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
@@ -164,8 +197,9 @@ internal class DownloadNotifier(private val context: Context) {
clearActions() clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
} }
notificationBuilder.show()
// Reset download information // Reset download information
isDownloading = false isDownloading = false
@@ -180,7 +214,7 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onError(error: String? = null, chapter: String? = null) { fun onError(error: String? = null, chapter: String? = null) {
// Create notification // Create notification
with(notificationBuilder) { with(errorNotificationBuilder) {
setContentTitle( setContentTitle(
chapter chapter
?: context.getString(R.string.download_notifier_downloader_title) ?: context.getString(R.string.download_notifier_downloader_title)
@@ -191,8 +225,9 @@ internal class DownloadNotifier(private val context: Context) {
setAutoCancel(false) setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
} }
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information // Reset download information
errorThrown = true errorThrown = true
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import androidx.core.content.edit
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.models.Chapter 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. * 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. * 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 // Last entry matches the manga, reuse it to avoid decoding json from preferences
lastEntry.copy(chapters = newChapters) lastEntry.copy(chapters = newChapters)
} else { } else {
val existingEntry = prefs.getString(manga.id!!.toString(), null) val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) { if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter // Existing entry found on preferences, decode json and add the new chapter
val savedEntry = gson.fromJson<Entry>(existingEntry) val savedEntry = gson.fromJson<Entry>(existingEntry)
@@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
// Save current state // Save current state
val json = gson.toJson(newEntry) val json = gson.toJson(newEntry)
prefs.edit().putString(newEntry.manga.id.toString(), json).apply() preferences.edit {
putString(newEntry.manga.id.toString(), json)
}
lastAddedEntry = newEntry lastAddedEntry = newEntry
} }
@@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
@Synchronized @Synchronized
fun getPendingChapters(): Map<Manga, List<Chapter>> { fun getPendingChapters(): Map<Manga, List<Chapter>> {
val entries = decodeAll() val entries = decodeAll()
prefs.edit().clear().apply() preferences.edit {
clear()
}
lastAddedEntry = null lastAddedEntry = null
return entries.associate { entry -> return entries.associate { entry ->
@@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
* Decodes all the chapters from preferences. * Decodes all the chapters from preferences.
*/ */
private fun decodeAll(): List<Entry> { private fun decodeAll(): List<Entry> {
return prefs.all.values.mapNotNull { rawEntry -> return preferences.all.values.mapNotNull { rawEntry ->
try { try {
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) } (rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
} catch (e: Exception) { } catch (e: Exception) {
@@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
private data class ChapterEntry( private data class ChapterEntry(
val id: Long, val id: Long,
val url: String, 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. * Returns a chapter entry from a chapter model.
*/ */
private fun Chapter.toEntry(): ChapterEntry { 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.id = id
it.url = url it.url = url
it.name = name it.name = name
it.scanlator = scanlator
} }
} }
} }
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads. * The root directory for downloads.
*/ */
private var downloadsDir = preferences.downloadsDirectory().get().let { 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) DiskUtil.createNoMediaFile(dir, context)
dir dir
} }
init { init {
preferences.downloadsDirectory().asFlow() preferences.downloadsDirectory().asFlow()
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } .onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
.launchIn(scope) .launchIn(scope)
} }
@@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source) 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> { fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() 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. * Returns the download directory name for a source.
* *
@@ -118,7 +150,9 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga to query. * @param manga the manga to query.
*/ */
fun getMangaDirName(manga: Manga): String { 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. * @param chapter the chapter to query.
*/ */
fun getChapterDirName(chapter: Chapter): String { 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.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo.State.CONNECTED import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build import android.os.Build
@@ -82,7 +83,7 @@ class DownloadService : Service() {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification()) startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
wakeLock = acquireWakeLock(javaClass.name) wakeLock = acquireWakeLock(javaClass.name)
runningRelay.call(true) runningRelay.call(true)
subscriptions = CompositeSubscription() subscriptions = CompositeSubscription()
@@ -143,7 +144,7 @@ class DownloadService : Service() {
private fun onNetworkStateChanged(connectivity: Connectivity) { private fun onNetworkStateChanged(connectivity: Connectivity) {
when (connectivity.state) { when (connectivity.state) {
CONNECTED -> { 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)) downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
} else { } else {
val started = downloadManager.startDownloads() val started = downloadManager.startDownloads()
@@ -175,19 +176,19 @@ class DownloadService : Service() {
/** /**
* Releases the wake lock if it's held. * Releases the wake lock if it's held.
*/ */
fun PowerManager.WakeLock.releaseIfNeeded() { private fun PowerManager.WakeLock.releaseIfNeeded() {
if (isHeld) release() if (isHeld) release()
} }
/** /**
* Acquires the wake lock if it's not held. * Acquires the wake lock if it's not held.
*/ */
fun PowerManager.WakeLock.acquireIfNeeded() { private fun PowerManager.WakeLock.acquireIfNeeded() {
if (!isHeld) acquire() if (!isHeld) acquire()
} }
private fun getPlaceholderNotification(): Notification { private fun getPlaceholderNotification(): Notification {
return notification(Notifications.CHANNEL_DOWNLOADER) { return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setContentTitle(getString(R.string.download_notifier_downloader_title)) setContentTitle(getString(R.string.download_notifier_downloader_title))
} }
} }
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import androidx.core.content.edit
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -42,9 +43,9 @@ class DownloadStore(
* @param downloads the list of downloads to add. * @param downloads the list of downloads to add.
*/ */
fun addAll(downloads: List<Download>) { fun addAll(downloads: List<Download>) {
val editor = preferences.edit() preferences.edit {
downloads.forEach { editor.putString(getKey(it), serialize(it)) } downloads.forEach { putString(getKey(it), serialize(it)) }
editor.apply() }
} }
/** /**
@@ -53,14 +54,18 @@ class DownloadStore(
* @param download the download to remove. * @param download the download to remove.
*/ */
fun remove(download: Download) { fun remove(download: Download) {
preferences.edit().remove(getKey(download)).apply() preferences.edit {
remove(getKey(download))
}
} }
/** /**
* Removes all the downloads from the store. * Removes all the downloads from the store.
*/ */
fun clear() { fun clear() {
preferences.edit().clear().apply() preferences.edit {
clear()
}
} }
/** /**
@@ -137,9 +137,10 @@ class Downloader(
} else { } else {
if (notifier.paused) { if (notifier.paused) {
notifier.paused = false notifier.paused = false
notifier.onDownloadPaused() notifier.onPaused()
} else { } else {
notifier.dismiss() notifier.dismissProgress()
notifier.onComplete()
} }
} }
} }
@@ -170,7 +171,7 @@ class Downloader(
.forEach { it.status = Download.NOT_DOWNLOADED } .forEach { it.status = Download.NOT_DOWNLOADED }
} }
queue.clear() queue.clear()
notifier.dismiss() notifier.dismissProgress()
} }
/** /**
@@ -231,13 +232,9 @@ class Downloader(
val wasEmpty = queue.isEmpty() val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF. // Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async { val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source)
chapters chapters
// Avoid downloading chapters with the same name.
.distinctBy { it.name }
// Filter out those already downloaded. // 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. // Add chapters to queue from the start.
.sortedByDescending { it.source_order } .sortedByDescending { it.source_order }
} }
@@ -270,8 +267,16 @@ class Downloader(
* @param download the chapter to be downloaded. * @param download the chapter to be downloaded.
*/ */
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) val mangaDir = provider.getMangaDir(download.manga, download.source)
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 tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
@@ -489,5 +494,8 @@ class Downloader(
companion object { companion object {
const val TMP_DIR_SUFFIX = "_tmp" 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 mangaId id of manga
* @param chapterId id of chapter * @param chapterId id of chapter
*/ */
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) { private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
val db = DatabaseHelper(context) val db = DatabaseHelper(context)
val manga = db.getManga(mangaId).executeAsBlocking() val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking()
@@ -30,8 +30,12 @@ object Notifications {
/** /**
* Notification channel and ids used by the downloader. * Notification channel and ids used by the downloader.
*/ */
const val CHANNEL_DOWNLOADER = "downloader_channel" private const val GROUP_DOWNLOADER = "group_downloader"
const val ID_DOWNLOAD_CHAPTER = -201 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 const val ID_DOWNLOAD_CHAPTER_ERROR = -202
/** /**
@@ -50,7 +54,7 @@ object Notifications {
/** /**
* Notification channel and ids used by the backup/restore system. * 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 CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
const val ID_BACKUP_PROGRESS = -501 const val ID_BACKUP_PROGRESS = -501
const val ID_RESTORE_PROGRESS = -503 const val ID_RESTORE_PROGRESS = -503
@@ -59,6 +63,7 @@ object Notifications {
const val ID_RESTORE_COMPLETE = -504 const val ID_RESTORE_COMPLETE = -504
private val deprecatedChannels = listOf( private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel" "backup_restore_complete_channel"
) )
@@ -70,10 +75,12 @@ object Notifications {
fun createChannels(context: Context) { fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore)) listOf(
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup) 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( NotificationChannel(
CHANNEL_COMMON, context.getString(R.string.channel_common), CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
@@ -85,9 +92,24 @@ object Notifications {
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).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) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
@@ -99,26 +121,23 @@ object Notifications {
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
), ),
NotificationChannel( 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 NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_BACK_RESTORE group = GROUP_BACKUP_RESTORE
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( 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 NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
group = GROUP_BACK_RESTORE group = GROUP_BACKUP_RESTORE
setShowBadge(false) setShowBadge(false)
setSound(null, null) setSound(null, null)
} }
) ).forEach(context.notificationManager::createNotificationChannel)
context.notificationManager.createNotificationChannels(channels)
// Delete old notification channels // Delete old notification channels
deprecatedChannels.forEach { deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
context.notificationManager.deleteNotificationChannel(it)
}
} }
} }
@@ -55,6 +55,8 @@ object PreferenceKeys {
const val readWithTapping = "reader_tap" const val readWithTapping = "reader_tap"
const val readWithTappingInverted = "reader_tapping_inverted"
const val readWithLongTap = "reader_long_tap" const val readWithLongTap = "reader_long_tap"
const val readWithVolumeKeys = "reader_volume_keys" const val readWithVolumeKeys = "reader_volume_keys"
@@ -67,6 +69,8 @@ object PreferenceKeys {
const val landscapeColumns = "pref_library_columns_landscape_key" 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 updateOnlyNonCompleted = "pref_update_only_non_completed_key"
const val autoUpdateTrack = "pref_auto_update_manga_sync_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 removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
const val removeBookmarkedChapters = "pref_remove_bookmarked"
const val libraryUpdateInterval = "pref_library_update_interval_key" const val libraryUpdateInterval = "pref_library_update_interval_key"
const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateRestriction = "library_update_restriction"
@@ -117,6 +123,8 @@ object PreferenceKeys {
const val automaticExtUpdates = "automatic_ext_updates" const val automaticExtUpdates = "automatic_ext_updates"
const val allowNsfwSource = "allow_nsfw_source"
const val startScreen = "start_screen" const val startScreen = "start_screen"
const val useBiometricLock = "use_biometric_lock" const val useBiometricLock = "use_biometric_lock"
@@ -179,8 +187,6 @@ object PreferenceKeys {
const val eh_lock_manually = "eh_lock_manually" const val eh_lock_manually = "eh_lock_manually"
const val eh_nh_useHighQualityThumbs = "eh_nh_hq_thumbs"
const val eh_showSyncIntro = "eh_show_sync_intro" const val eh_showSyncIntro = "eh_show_sync_intro"
const val eh_readOnlySync = "eh_sync_read_only" const val eh_readOnlySync = "eh_sync_read_only"
@@ -233,8 +239,6 @@ object PreferenceKeys {
const val eh_aggressivePageLoading = "eh_aggressive_page_loading" const val eh_aggressivePageLoading = "eh_aggressive_page_loading"
const val eh_hl_useHighQualityThumbs = "eh_hl_hq_thumbs"
const val eh_preload_size = "eh_preload_size" const val eh_preload_size = "eh_preload_size"
const val eh_tag_filtering_value = "eh_tag_filtering_value" const val eh_tag_filtering_value = "eh_tag_filtering_value"
@@ -243,8 +247,6 @@ object PreferenceKeys {
const val eh_is_hentai_enabled = "eh_is_hentai_enabled" 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_use_auto_webtoon = "eh_use_auto_webtoon"
const val eh_watched_list_default_state = "eh_watched_list_default_state" 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 sources_tab_source_categories = "sources_tab_source_categories"
const val sourcesSort = "sources_sort" 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, COMFORTABLE_GRID,
LIST, LIST,
} }
enum class TappingInvertMode {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
}
} }
@@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.preference package eu.kanade.tachiyomi.data.preference
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.tfcporciuncula.flow.FlowSharedPreferences import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference 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.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import java.io.File import java.io.File
@@ -27,27 +28,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
.onEach { block(it) } .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) @OptIn(ExperimentalCoroutinesApi::class)
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val flowPrefs = FlowSharedPreferences(prefs) val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile( private val defaultDownloadsDir = File(
File( Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name),
context.getString(R.string.app_name), "downloads"
"downloads" ).toUri()
)
)
private val defaultBackupDir = Uri.fromFile( private val defaultBackupDir = File(
File( Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name),
context.getString(R.string.app_name), "backup"
"backup" ).toUri()
)
)
fun startScreen() = prefs.getInt(Keys.startScreen, 1) fun startScreen() = prefs.getInt(Keys.startScreen, 1)
@@ -109,7 +114,7 @@ class PreferencesHelper(val context: Context) {
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1) fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1) fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 3)
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true) fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
@@ -121,6 +126,8 @@ class PreferencesHelper(val context: Context) {
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true) fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true) fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false) fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
@@ -131,6 +138,8 @@ class PreferencesHelper(val context: Context) {
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0) fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false) fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
@@ -179,6 +188,8 @@ class PreferencesHelper(val context: Context) {
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false) fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24) fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
@@ -214,6 +225,8 @@ class PreferencesHelper(val context: Context) {
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true) fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0) fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0) fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
@@ -299,8 +312,6 @@ class PreferencesHelper(val context: Context) {
fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "") fun eh_sessionCookie() = flowPrefs.getString(Keys.eh_sessionCookie, "")
fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "") fun eh_hathPerksCookies() = flowPrefs.getString(Keys.eh_hathPerksCookie, "")
fun eh_nh_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_nh_useHighQualityThumbs, false)
fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true) fun eh_showSyncIntro() = flowPrefs.getBoolean(Keys.eh_showSyncIntro, true)
fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false) fun eh_readOnlySync() = flowPrefs.getBoolean(Keys.eh_readOnlySync, false)
@@ -343,12 +354,8 @@ class PreferencesHelper(val context: Context) {
fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false) fun eh_aggressivePageLoading() = flowPrefs.getBoolean(Keys.eh_aggressivePageLoading, false)
fun eh_hl_useHighQualityThumbs() = flowPrefs.getBoolean(Keys.eh_hl_useHighQualityThumbs, false)
fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4) fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4)
fun eh_useNewMangaInterface() = flowPrefs.getBoolean(Keys.eh_use_new_manga_interface, true)
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true) fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
fun eh_watchedListDefaultState() = flowPrefs.getBoolean(Keys.eh_watched_list_default_state, false) 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 sourcesTabSourcesInCategories() = flowPrefs.getStringSet(Keys.sources_tab_source_categories, mutableSetOf())
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0) 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 package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceDataStore
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() { class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
@@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putBoolean(key: String?, value: Boolean) { override fun putBoolean(key: String?, value: Boolean) {
prefs.edit().putBoolean(key, value).apply() prefs.edit {
putBoolean(key, value)
}
} }
override fun getInt(key: String?, defValue: Int): Int { override fun getInt(key: String?, defValue: Int): Int {
@@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putInt(key: String?, value: Int) { override fun putInt(key: String?, value: Int) {
prefs.edit().putInt(key, value).apply() prefs.edit {
putInt(key, value)
}
} }
override fun getLong(key: String?, defValue: Long): Long { override fun getLong(key: String?, defValue: Long): Long {
@@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putLong(key: String?, value: Long) { override fun putLong(key: String?, value: Long) {
prefs.edit().putLong(key, value).apply() prefs.edit {
putLong(key, value)
}
} }
override fun getFloat(key: String?, defValue: Float): Float { override fun getFloat(key: String?, defValue: Float): Float {
@@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putFloat(key: String?, value: Float) { override fun putFloat(key: String?, value: Float) {
prefs.edit().putFloat(key, value).apply() prefs.edit {
putFloat(key, value)
}
} }
override fun getString(key: String?, defValue: String?): String? { override fun getString(key: String?, defValue: String?): String? {
@@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putString(key: String?, value: String?) { override fun putString(key: String?, value: String?) {
prefs.edit().putString(key, value).apply() prefs.edit {
putString(key, value)
}
} }
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? { override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
@@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
} }
override fun putStringSet(key: String?, values: MutableSet<String>?) { override fun putStringSet(key: String?, values: MutableSet<String>?) {
prefs.edit().putStringSet(key, values).apply() prefs.edit {
putStringSet(key, values)
}
} }
} }
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject import com.github.salomonbrys.kotson.jsonObject
@@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return baseMangaUrl + mediaId return baseMangaUrl + mediaId
} }
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson import com.google.gson.Gson
@@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse( val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" .toUri()
).buildUpon() .buildUpon()
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
val request = Request.Builder() val request = Request.Builder()
@@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return "$baseMangaUrl/$remoteId" return "$baseMangaUrl/$remoteId"
} }
fun authUrl() = fun authUrl(): Uri =
Uri.parse(loginUrl).buildUpon() loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist 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.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch 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 mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun loginUrl() = Uri.parse(baseUrl).buildUpon() private fun loginUrl() = baseUrl.toUri().buildUpon()
.appendPath("login.php") .appendPath("login.php")
.toString() .toString()
private fun searchUrl(query: String): String { private fun searchUrl(query: String): String {
val col = "c[]" val col = "c[]"
return Uri.parse(baseUrl).buildUpon() return baseUrl.toUri().buildUpon()
.appendPath("manga.php") .appendPath("manga.php")
.appendQueryParameter("q", query) .appendQueryParameter("q", query)
.appendQueryParameter(col, "a") .appendQueryParameter(col, "a")
@@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.toString() .toString()
} }
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon() private fun exportListUrl() = baseUrl.toUri().buildUpon()
.appendPath("panel.php") .appendPath("panel.php")
.appendQueryParameter("go", "export") .appendQueryParameter("go", "export")
.toString() .toString()
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
.appendPath(mediaId.toString()) .appendPath(mediaId.toString())
.appendPath("edit") .appendPath("edit")
.toString() .toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
.appendPath("add.json") .appendPath("add.json")
.toString() .toString()
@@ -476,7 +476,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
fun copyPersonalFrom(track: Track) { fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString() num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt() val numScore = track.score.toInt()
if (numScore in 1..9) { if (numScore == 0) {
score = ""
} else if (numScore in 1..10) {
score = numScore.toString() score = numScore.toString()
} }
status = track.status.toString() status = track.status.toString()
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori 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.array
import com.github.salomonbrys.kotson.jsonObject import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullString 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 updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse("$apiUrl/mangas").buildUpon() val url = "$apiUrl/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity") .appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
@@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun findLibManga(track: Track, user_id: String): Observable<Track?> { 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("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
@@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.get() .get()
.build() .build()
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val requestMangas = Request.Builder() val requestMangas = Request.Builder()
@@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun authUrl() = fun authUrl() =
Uri.parse(loginUrl).buildUpon() loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
@@ -26,7 +26,7 @@ class DevRepoUpdateChecker : UpdateChecker() {
override suspend fun checkForUpdate(): UpdateResult { override suspend fun checkForUpdate(): UpdateResult {
val response = withContext(Dispatchers.IO) { val response = withContext(Dispatchers.IO) {
client.newCall(GET(DevRepoRelease.LATEST_URL)).await(assertSuccess = false) client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
} }
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk" // Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
@@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep 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.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HBROWSE_SOURCE_ID
import exh.HITOMI_SOURCE_ID
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.NHENTAI_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import kotlinx.coroutines.async import kotlinx.coroutines.async
import rx.Observable import rx.Observable
@@ -83,12 +78,6 @@ class ExtensionManager(
return when (source.id) { return when (source.id) {
EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source) EXH_SOURCE_ID -> context.getDrawable(R.mipmap.ic_ehentai_source)
PERV_EDEN_EN_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
PERV_EDEN_IT_SOURCE_ID -> context.getDrawable(R.mipmap.ic_perveden_source)
NHENTAI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_nhentai_source)
HITOMI_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hitomi_source)
EIGHTMUSES_SOURCE_ID -> context.getDrawable(R.mipmap.ic_8muses_source)
HBROWSE_SOURCE_ID -> context.getDrawable(R.mipmap.ic_hbrowse_source)
MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source) MERGED_SOURCE_ID -> context.getDrawable(R.mipmap.ic_merged_source)
else -> null else -> null
} }
@@ -319,8 +308,7 @@ class ExtensionManager(
if (signature !in untrustedSignatures) return if (signature !in untrustedSignatures) return
ExtensionLoader.trustedSignatures += signature ExtensionLoader.trustedSignatures += signature
val preference = preferences.trustedSignatures() preferences.trustedSignatures() += signature
preference.set(preference.get() + signature)
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature } val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
untrustedExtensions -= nowTrustedExtensions untrustedExtensions -= nowTrustedExtensions
@@ -1,44 +1,30 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import java.util.Date import java.util.Date
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
private val network: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val gson: Gson by injectLazy()
suspend fun findExtensions(): List<Extension.Available> { suspend fun findExtensions(): List<Extension.Available> {
val call = GET(EXT_URL) val service: ExtensionGithubService = ExtensionGithubService.create()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val response = network.client.newCall(call).await() val response = service.getRepo()
if (response.isSuccessful) { parseResponse(response)
parseResponse(response)
} else {
response.close()
throw Exception("Failed to get extensions")
}
} }
} }
@@ -72,11 +58,7 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun parseResponse(response: Response): List<Extension.Available> { private fun parseResponse(json: JsonArray): List<Extension.Available> {
val text = response.body?.use { it.string() } ?: return emptyList()
val json = gson.fromJson<JsonArray>(text)
return json return json
.filter { element -> .filter { element ->
val versionName = element["version"].string val versionName = element["version"].string
@@ -90,14 +72,15 @@ internal class ExtensionGithubApi {
val versionName = element["version"].string val versionName = element["version"].string
val versionCode = element["code"].int val versionCode = element["code"].int
val lang = element["lang"].string val lang = element["lang"].string
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}" val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL/apk/${extension.apkName}" return "$REPO_URL_PREFIX/apk/${extension.apkName}"
} }
// SY --> // SY -->
@@ -110,7 +93,7 @@ internal class ExtensionGithubApi {
// SY <-- // SY <--
companion object { companion object {
private const val REPO_URL = "https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo" const val BASE_URL = "https://raw.githubusercontent.com/"
private const val EXT_URL = "$REPO_URL/index.json" const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
} }
} }
@@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.extension.api
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.network.NetworkHelper
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.json.gz")
suspend fun getRepo(): JsonArray
}
@@ -9,14 +9,16 @@ sealed class Extension {
abstract val versionName: String abstract val versionName: String
abstract val versionCode: Int abstract val versionCode: Int
abstract val lang: String? abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed( data class Installed(
override val name: String, override val name: String,
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val sources: List<Source>,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false, val hasUpdate: Boolean = false,
val isObsolete: Boolean = false, val isObsolete: Boolean = false,
val isUnofficial: Boolean = false, val isUnofficial: Boolean = false,
@@ -31,6 +33,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String val iconUrl: String
) : Extension() ) : Extension()
@@ -41,6 +44,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val signatureHash: String, val signatureHash: String,
override val lang: String? = null override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension() ) : Extension()
} }
@@ -7,6 +7,8 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep 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 // Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register() downloadReceiver.register()
val downloadUri = Uri.parse(url) val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri) val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name) .setTitle(extension.name)
.setMimeType(APK_MIME) .setMimeType(APK_MIME)
@@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param pkgName The package name of the extension to uninstall * @param pkgName The package name of the extension to uninstall
*/ */
fun uninstallApk(pkgName: String) { fun uninstallApk(pkgName: String) {
val packageUri = Uri.parse("package:$pkgName") val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) context.startActivity(intent)
@@ -5,6 +5,8 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -15,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
/** /**
* Class that handles the loading of the extensions installed in the system. * Class that handles the loading of the extensions installed in the system.
@@ -24,20 +25,25 @@ import uy.kohesive.injekt.api.get
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader { internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy()
private val allowNsfwSource by lazy {
preferences.allowNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension" private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2 const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2 const val LIB_VERSION_MAX = 1.2
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key // inorichi's key
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@@ -125,6 +131,11 @@ internal object ExtensionLoader {
return LoadResult.Untrusted(extension) return LoadResult.Untrusted(extension)
} }
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed")
}
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
@@ -141,7 +152,13 @@ internal object ExtensionLoader {
try { try {
when (val obj = Class.forName(it, false, classLoader).newInstance()) { when (val obj = Class.forName(it, false, classLoader).newInstance()) {
is Source -> listOf(obj) is Source -> listOf(obj)
is SourceFactory -> obj.createSources() is SourceFactory -> {
if (isSourceNsfw(obj)) {
emptyList()
} else {
obj.createSources()
}
}
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -149,10 +166,11 @@ internal object ExtensionLoader {
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
.filter { !isSourceNsfw(it) }
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang } .map { it.lang }
.toSet() .toSet()
val lang = when (langs.size) { val lang = when (langs.size) {
0 -> "" 0 -> ""
1 -> langs.first() 1 -> langs.first()
@@ -160,7 +178,7 @@ internal object ExtensionLoader {
} }
val extension = Extension.Installed( val extension = Extension.Installed(
extName, pkgName, versionName, versionCode, sources, lang, extName, pkgName, versionName, versionCode, lang, isNsfw, sources,
isUnofficial = signatureHash != officialSignature isUnofficial = signatureHash != officialSignature
) )
return LoadResult.Success(extension) return LoadResult.Success(extension)
@@ -188,4 +206,22 @@ internal object ExtensionLoader {
null null
} }
} }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
return false
}
if (clazz !is Source && clazz !is SourceFactory) {
return false
}
// Annotations are proxied, hence this janky way of checking for them
return clazz.javaClass.annotations
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
.firstOrNull { it == Nsfw::class.java.simpleName } != null
}
} }
@@ -2,18 +2,19 @@ package eu.kanade.tachiyomi.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@@ -42,9 +43,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
@Synchronized @Synchronized
override fun intercept(chain: Interceptor.Chain): Response { 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 initWebView
val originalRequest = chain.request()
val response = chain.proceed(originalRequest) val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
@@ -85,7 +94,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
handler.post { handler.post {
val webview = WebView(context) val webview = WebView(context)
webView = webview webView = webview
webview.settings.javaScriptEnabled = true webview.setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent") 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 // HTTP error codes are only received since M
if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
url == origRequestUrl && !challengeFound url == origRequestUrl && !challengeFound
) { ) {
// The first request didn't return the challenge, abort. // The first request didn't return the challenge, abort.
@@ -113,13 +122,15 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
} }
override fun onReceivedHttpError( override fun onReceivedErrorCompat(
view: WebView, view: WebView,
request: WebResourceRequest, errorCode: Int,
errorResponse: WebResourceResponse description: String?,
failingUrl: String,
isMainFrame: Boolean
) { ) {
if (request.isForMainFrame) { if (isMainFrame) {
if (errorResponse.statusCode == 503) { if (errorCode == 503) {
// Found the Cloudflare challenge page. // Found the Cloudflare challenge page.
challengeFound = true challengeFound = true
} else { } else {
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.google.gson.GsonBuilder
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
@@ -28,13 +29,15 @@ import timber.log.Timber
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(private val context: Context) : CatalogueSource {
companion object { companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/" const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
private const val COVER_NAME = "cover.jpg" 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 POPULAR_FILTERS = FilterList(OrderBy())
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) }) private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
const val ID = 0L
fun updateCover(context: Context, manga: SManga, input: InputStream): File? { fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
val dir = getBaseDirectories(context).firstOrNull() val dir = getBaseDirectories(context).firstOrNull()
@@ -73,9 +76,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
val baseDirs = getBaseDirectories(context) val baseDirs = getBaseDirectories(context)
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L 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() .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 } .distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
@@ -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 fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
getBaseDirectories(context) getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() } .mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten() .flatten()
.firstOrNull { it.extension == "json" } .firstOrNull { it.extension == "json" }
@@ -154,6 +202,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
?: manga.genre ?: manga.genre
manga.status = json["status"]?.asInt ?: manga.status manga.status = json["status"]?.asInt ?: manga.status
} }
return Observable.just(manga) return Observable.just(manga)
} }
@@ -204,8 +253,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
var chapterNameIndex = 0 var chapterNameIndex = 0
var mangaTitleIndex = 0 var mangaTitleIndex = 0
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) { while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
val chapterChar = chapterName.get(chapterNameIndex) val chapterChar = chapterName[chapterNameIndex]
val mangaChar = mangaTitle.get(mangaTitleIndex) val mangaChar = mangaTitle[mangaTitleIndex]
if (!chapterChar.equals(mangaChar, true)) { if (!chapterChar.equals(mangaChar, true)) {
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace() val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace() val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
@@ -235,7 +284,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
private fun isSupportedFile(extension: String): Boolean { 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 { fun getFormat(chapter: SChapter): Format {
@@ -269,8 +318,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
return when (val format = getFormat(chapter)) { return when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) ?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) } entry?.let { updateCover(context, manga, it.inputStream()) }
} }
@@ -4,6 +4,7 @@ import android.content.Context
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EIGHTMUSES_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.HBROWSE_SOURCE_ID
import exh.HENTAI_CAFE_SOURCE_ID
import exh.PERV_EDEN_EN_SOURCE_ID import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID import exh.PERV_EDEN_IT_SOURCE_ID
import exh.metadata.metadata.PervEdenLang import exh.PURURIN_SOURCE_ID
import exh.TSUMINO_SOURCE_ID
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
@@ -31,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -52,7 +56,7 @@ open class SourceManager(private val context: Context) {
// SY --> // SY -->
// Recreate sources when they change // Recreate sources when they change
prefs.enableExhentai().asFlow().onEach { prefs.enableExhentai().asImmediateFlow {
createEHSources().forEach { registerSource(it) } createEHSources().forEach { registerSource(it) }
}.launchIn(scope) }.launchIn(scope)
@@ -72,6 +76,10 @@ open class SourceManager(private val context: Context) {
fun getOnlineSources() = sourcesMap.values.filterIsInstance<HttpSource>() 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>() fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
// SY --> // SY -->
@@ -98,9 +106,9 @@ open class SourceManager(private val context: Context) {
XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName) XLog.d("[EXH] Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
val enhancedSource = EnhancedHttpSource( val enhancedSource = EnhancedHttpSource(
source, 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) currentDelegatedSources.plusAssign(map)
enhancedSource enhancedSource
} else source } else source
@@ -132,12 +140,6 @@ open class SourceManager(private val context: Context) {
if (prefs.enableExhentai().get()) { if (prefs.enableExhentai().get()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context) exSrcs += EHentai(EXH_SOURCE_ID, true, context)
} }
exSrcs += PervEden(PERV_EDEN_EN_SOURCE_ID, PervEdenLang.en)
exSrcs += PervEden(PERV_EDEN_IT_SOURCE_ID, PervEdenLang.it)
exSrcs += NHentai(context)
exSrcs += Hitomi()
exSrcs += EightMuses()
exSrcs += HBrowse()
return exSrcs return exSrcs
} }
// SY <-- // SY <--
@@ -170,23 +172,23 @@ open class SourceManager(private val context: Context) {
// SY --> // SY -->
companion object { companion object {
private const val fillInSourceId = 9999L private const val fillInSourceId = Long.MAX_VALUE
val DELEGATED_SOURCES = listOf( val DELEGATED_SOURCES = listOf(
DelegatedSource( DelegatedSource(
"Hentai Cafe", "Hentai Cafe",
260868874183818481, HENTAI_CAFE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe", "eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
HentaiCafe::class HentaiCafe::class
), ),
DelegatedSource( DelegatedSource(
"Pururin", "Pururin",
2221515250486218861, PURURIN_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.pururin.Pururin", "eu.kanade.tachiyomi.extension.en.pururin.Pururin",
Pururin::class Pururin::class
), ),
DelegatedSource( DelegatedSource(
"Tsumino", "Tsumino",
6707338697138388238, TSUMINO_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class Tsumino::class
)/*, )/*,
@@ -196,10 +198,48 @@ open class SourceManager(private val context: Context) {
"eu.kanade.tachiyomi.extension.all.mangadex", "eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class, MangaDex::class,
true 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 } ).associateBy { it.originalSourceQualifiedClassName }
var currentDelegatedSources = mutableMapOf<String, DelegatedSource>() var currentDelegatedSources = mutableMapOf<Long, DelegatedSource>()
data class DelegatedSource( data class DelegatedSource(
val sourceName: String, val sourceName: String,
@@ -29,7 +29,9 @@ sealed class Filter<T>(val name: String, var state: T) {
data class Selection(val index: Int, val ascending: Boolean) 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) 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 { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -1,3 +1,9 @@
package eu.kanade.tachiyomi.source.model 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 package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import java.io.Serializable import java.io.Serializable
interface SManga : Serializable { interface SManga : Serializable {
@@ -22,27 +23,40 @@ interface SManga : Serializable {
var initialized: Boolean 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) { fun copyFrom(other: SManga) {
// EXH --> // EXH -->
if (other.title.isNotBlank()) { if (other.title.isNotBlank()) {
title = other.title title = other.originalTitle
} }
// EXH <-- // EXH <--
if (other.author != null) { if (other.author != null) {
author = other.author author = /* SY --> */ other.originalAuthor /* SY <-- */
} }
if (other.artist != null) { if (other.artist != null) {
artist = other.artist artist = /* SY --> */ other.originalArtist /* SY <-- */
} }
if (other.description != null) { if (other.description != null) {
description = other.description description = /* SY --> */ other.originalDescription /* SY <-- */
} }
if (other.genre != null) { if (other.genre != null) {
genre = other.genre genre = /* SY --> */ other.originalGenre /* SY <-- */
} }
if (other.thumbnail_url != null) { if (other.thumbnail_url != null) {
@@ -61,9 +75,6 @@ interface SManga : Serializable {
const val ONGOING = 1 const val ONGOING = 1
const val COMPLETED = 2 const val COMPLETED = 2
const val LICENSED = 3 const val LICENSED = 3
// SY -->
const val RECOMMENDS = 69 // nice
// SY <--
fun create(): SManga { fun create(): SManga {
return SMangaImpl() return SMangaImpl()
@@ -1,14 +1,18 @@
package eu.kanade.tachiyomi.source.online package eu.kanade.tachiyomi.source.online
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.source.EnhancedHttpSource
import kotlin.reflect.KClass import kotlin.reflect.KClass
import rx.Completable import rx.Completable
import rx.Single 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 SManga.id get() = (this as? Manga)?.id
val SChapter.mangaId get() = (this as? Chapter)?.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.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHTags 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.EH_GENRE_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT 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_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.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.metadata.nullIfBlank
import exh.metadata.parseHumanReadableByteCount import exh.metadata.parseHumanReadableByteCount
import exh.ui.login.LoginController import exh.ui.login.LoginController
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
import exh.util.asObservableWithAsyncStacktrace import exh.util.asObservableWithAsyncStacktrace
import exh.util.ignore import exh.util.ignore
import exh.util.nullIfBlank
import exh.util.trimOrNull
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder import java.net.URLEncoder
import java.util.ArrayList import java.util.ArrayList
@@ -91,9 +96,9 @@ class EHentai(
/** /**
* Gallery list entry * 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) // Parse mangas (supports compact + extended layout)
val parsedMangas = select(".itg > tbody > tr").filter { val parsedMangas = select(".itg > tbody > tr").filter {
// Do not parse header and ads // Do not parse header and ads
@@ -102,8 +107,11 @@ class EHentai(
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img") val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img")
val column2 = it.selectFirst(".gl3e, .gl2c") val column2 = it.selectFirst(".gl3e, .gl2c")
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a") 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 favElement = column2.children().find { it.attr("style").startsWith("border-color") }
val infoElements = infoElement?.select("div")
val parsedTags = mutableListOf<RaisedTag>()
ParsedManga( ParsedManga(
fav = FAVORITES_BORDER_HEX_COLORS.indexOf( fav = FAVORITES_BORDER_HEX_COLORS.indexOf(
@@ -116,7 +124,78 @@ class EHentai(
// Get image // Get image
thumbnail_url = thumbnailElement.attr("src") 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) 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 * Parse a list of galleries
*/ */
fun genericMangaParse(response: Response) = extendedGenericMangaParse(response.asJsoup()).let { 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) {} override fun fetchChapterList(manga: SManga) = fetchChapterList(manga) {}
@@ -271,7 +393,7 @@ class EHentai(
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(context, query) {
searchMangaRequestObservable(page, query, filters).flatMap { searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess() client.newCall(it).asObservableSuccess()
}.map { response -> }.map { response ->
@@ -477,6 +599,8 @@ class EHentai(
element.text().trim(), element.text().trim(),
if (element.hasClass("gtl")) { if (element.hasClass("gtl")) {
TAG_TYPE_LIGHT TAG_TYPE_LIGHT
} else if (element.hasClass("gtw")) {
TAG_TYPE_WEAK
} else { } else {
TAG_TYPE_NORMAL TAG_TYPE_NORMAL
} }
@@ -550,7 +674,7 @@ class EHentai(
page++ page++
} while (parsed.second) } while (parsed.second)
return Pair(result as List<ParsedManga>, favNames!!) return Pair(result.toList(), favNames!!)
} }
fun spPref() = if (exh) { fun spPref() = if (exh) {
@@ -832,10 +956,16 @@ class EHentai(
return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/" return "${uri.scheme}://${uri.host}/g/${obj["gid"].int}/${obj["token"].string}/"
} }
override fun getDescriptionAdapter(controller: MangaController): EHentaiDescriptionAdapter {
return EHentaiDescriptionAdapter(controller)
}
companion object { companion object {
private const val QUERY_PREFIX = "?f_apply=Apply+Filter" private const val QUERY_PREFIX = "?f_apply=Apply+Filter"
private const val TR_SUFFIX = "TR" private const val TR_SUFFIX = "TR"
private const val REVERSE_PARAM = "TEH_REVERSE" 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 const val EH_API_BASE = "https://api.e-hentai.org/api.php"
private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!! private val JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!!
@@ -1,99 +1,51 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.HITOMI_SOURCE_ID
import exh.hitomi.HitomiNozomi
import exh.metadata.metadata.HitomiSearchMetadata import exh.metadata.metadata.HitomiSearchMetadata
import exh.metadata.metadata.HitomiSearchMetadata.Companion.BASE_URL import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.HitomiSearchMetadata.Companion.LTN_BASE_URL
import exh.metadata.metadata.HitomiSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HitomiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.vepta.vdm.ByteCursor
import rx.Observable import rx.Observable
import rx.Single
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
/** class Hitomi(delegate: HttpSource, val context: Context) :
* Man, I hate this source :( DelegatedHttpSource(delegate),
*/ LewdSource<HitomiSearchMetadata, Document>,
class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImportableSource { UrlImportableSource {
private val prefs: PreferencesHelper by injectLazy()
override val id = HITOMI_SOURCE_ID
/**
* Whether the source has support for latest updates.
*/
override val supportsLatest = true
/**
* Name of the source.
*/
override val name = "hitomi.la"
/**
* The class of the metadata used by this source
*/
override val metaClass = HitomiSearchMetadata::class override val metaClass = HitomiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang
override val id: Long
get() = if (delegate.lang == "other") otherId else delegate.id
private var cachedTagIndexVersion: Long? = null // Support direct URL importing
private var tagIndexVersionCacheTime: Long = 0 override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
private fun tagIndexVersion(): Single<Long> { urlImportFetchSearchManga(context, query) {
val sCachedTagIndexVersion = cachedTagIndexVersion super.fetchSearchManga(page, query, filters)
return if (sCachedTagIndexVersion == null ||
tagIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "tagindex").subscribeOn(Schedulers.io()).doOnNext {
cachedTagIndexVersion = it
tagIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedTagIndexVersion)
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
}
} }
private var cachedGalleryIndexVersion: Long? = null
private var galleryIndexVersionCacheTime: Long = 0
private fun galleryIndexVersion(): Single<Long> {
val sCachedGalleryIndexVersion = cachedGalleryIndexVersion
return if (sCachedGalleryIndexVersion == null ||
galleryIndexVersionCacheTime + INDEX_VERSION_CACHE_TIME_MS < System.currentTimeMillis()
) {
HitomiNozomi.getIndexVersion(client, "galleriesindex").subscribeOn(Schedulers.io()).doOnNext {
cachedGalleryIndexVersion = it
galleryIndexVersionCacheTime = System.currentTimeMillis()
}.toSingle()
} else {
Single.just(sCachedGalleryIndexVersion)
}
}
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: HitomiSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = input.location() url = input.location()
@@ -106,306 +58,63 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
title = galleryElement.selectFirst("h1").text() title = galleryElement.selectFirst("h1").text()
artists = galleryElement.select("h2 a").map { it.text() } artists = galleryElement.select("h2 a").map { it.text() }
tags += artists.map { RaisedTag("artist", it, TAG_TYPE_VIRTUAL) } tags += artists.map { RaisedTag("artist", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL) }
input.select(".gallery-info tr").forEach { input.select(".gallery-info tr").forEach {
val content = it.child(1) val content = it.child(1)
when (it.child(0).text().toLowerCase()) { when (it.child(0).text().toLowerCase()) {
"group" -> { "group" -> {
group = content.text() group = content.text()
tags += RaisedTag("group", group!!, TAG_TYPE_VIRTUAL) tags += RaisedTag("group", group!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
"type" -> { "type" -> {
type = content.text() type = content.text()
tags += RaisedTag("type", type!!, TAG_TYPE_VIRTUAL) tags += RaisedTag("type", type!!, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
"series" -> { "series" -> {
series = content.select("a").map { it.text() } series = content.select("a").map { it.text() }
tags += series.map { tags += series.map {
RaisedTag("series", it, TAG_TYPE_VIRTUAL) RaisedTag("series", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
} }
"language" -> { "language" -> {
language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1) language = content.selectFirst("a")?.attr("href")?.split('-')?.get(1)
language?.let { language?.let {
tags += RaisedTag("language", it, TAG_TYPE_VIRTUAL) tags += RaisedTag("language", it, RaisedSearchMetadata.TAG_TYPE_VIRTUAL)
} }
} }
"characters" -> { "characters" -> {
characters = content.select("a").map { it.text() } characters = content.select("a").map { it.text() }
tags += characters.map { RaisedTag("character", it, TAG_TYPE_DEFAULT) } tags += characters.map {
RaisedTag(
"character", it,
HitomiSearchMetadata.TAG_TYPE_DEFAULT
)
}
} }
"tags" -> { "tags" -> {
tags += content.select("a").map { tags += content.select("a").map {
val ns = if (it.attr("href").startsWith("/tag/male")) "male" val ns = if (it.attr("href").startsWith("/tag/male")) "male"
else if (it.attr("href").startsWith("/tag/female")) "female" else if (it.attr("href").startsWith("/tag/female")) "female"
else "misc" else "misc"
RaisedTag(ns, it.text().dropLast(if (ns == "misc") 0 else 2), TAG_TYPE_DEFAULT) RaisedTag(
ns, it.text().dropLast(if (ns == "misc") 0 else 2),
HitomiSearchMetadata.TAG_TYPE_DEFAULT
)
} }
} }
} }
} }
uploadDate = DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time uploadDate = try {
DATE_FORMAT.parse(input.selectFirst(".gallery-info .date").text())!!.time
} catch (e: Exception) {
null
}
} }
} }
override val lang = "all" override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
override val baseUrl = BASE_URL
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int) = HitomiNozomi.rangedGet(
"$LTN_BASE_URL/popular-all.nozomi",
100L * (page - 1),
99L + 100 * (page - 1)
)
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return urlImportFetchSearchManga(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 val matchingHosts = listOf( override val matchingHosts = listOf(
"hitomi.la" "hitomi.la"
@@ -421,11 +130,12 @@ class Hitomi : HttpSource(), LewdSource<HitomiSearchMetadata, Document>, UrlImpo
return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html" return "https://hitomi.la/manga/${uri.pathSegments[1].substringBefore('.')}.html"
} }
companion object { override fun getDescriptionAdapter(controller: MangaController): HitomiDescriptionAdapter {
private val INDEX_VERSION_CACHE_TIME_MS = 1000 * 60 * 10 return HitomiDescriptionAdapter(controller)
private val PAGE_SIZE = 25 }
private val NUMBER_OF_FRONTENDS = 2
companion object {
const val otherId = 2703068117101782422L
private val DATE_FORMAT by lazy { private val DATE_FORMAT by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US) SimpleDateFormat("yyyy-MM-dd HH:mm:ssX", Locale.US)
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -11,7 +12,7 @@ import exh.source.DelegatedHttpSource
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import rx.Observable import rx.Observable
class MangaDex(delegate: HttpSource) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
ConfigurableSource, ConfigurableSource,
UrlImportableSource { UrlImportableSource {
@@ -19,7 +20,7 @@ class MangaDex(delegate: HttpSource) :
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org") override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) 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.nullObj
import com.github.salomonbrys.kotson.nullString import com.github.salomonbrys.kotson.nullString
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.NHENTAI_SOURCE_ID
import exh.metadata.metadata.NHentaiSearchMetadata import exh.metadata.metadata.NHentaiSearchMetadata
import exh.metadata.metadata.NHentaiSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.NHentaiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
/** open class NHentai(delegate: HttpSource, val context: Context) :
* NHentai source DelegatedHttpSource(delegate),
*/ LewdSource<NHentaiSearchMetadata, Response>,
UrlImportableSource {
class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata, Response>, UrlImportableSource {
override val metaClass = NHentaiSearchMetadata::class override val metaClass = NHentaiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override val id: Long
// TODO There is currently no way to get the most popular mangas get() = if (delegate.lang == "other") otherId else delegate.id
// TODO Instead, we delegate this to the latest updates thing to avoid confusing users with an empty screen
return fetchLatestUpdates(page)
}
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
val trimmedIdQuery = query.trim().removePrefix("id:") urlImportFetchSearchManga(context, query) {
val newQuery = if (trimmedIdQuery.toIntOrNull() ?: -1 >= 0) { super.fetchSearchManga(page, query, filters)
"$baseUrl/g/$trimmedIdQuery/"
} else query
return urlImportFetchSearchManga(newQuery) {
searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess()
}.map { response ->
searchMangaParse(response)
}
}
}
private fun searchMangaRequestObservable(page: Int, query: String, filters: FilterList): Observable<Request> {
val advQuery = combineQuery(filters)
val favoriteFilter = filters.findInstance<FavoriteFilter>()
val uploadedFilter = filters.findInstance<UploadedFilter>()
val url: HttpUrl.Builder
if (favoriteFilter != null && favoriteFilter.state) {
url = "$baseUrl/favorites".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("q", "$query $advQuery")
.addQueryParameter("page", page.toString())
} else {
url = "$baseUrl/search".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("q", "$query $advQuery")
.addQueryParameter("page", page.toString())
if (uploadedFilter?.state?.isBlank() == true) {
filters.findInstance<SortFilter>()?.let { f ->
url.addQueryParameter("sort", f.toUriPart())
}
}
} }
return client.newCall(nhGet(url.toString()))
.asObservableSuccess()
.map { nhGet(url.toString(), page) }
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = parseResultPage(response)
override fun latestUpdatesRequest(page: Int): Request {
val uri = Uri.parse(baseUrl).buildUpon()
uri.appendQueryParameter("page", page.toString())
return nhGet(uri.toString(), page)
}
override fun latestUpdatesParse(response: Response) = parseResultPage(response)
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -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) { override fun parseIntoMetadata(metadata: NHentaiSearchMetadata, input: Response) {
val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(UNICODE_ESCAPE_REGEX) { it.groupValues[1].toInt(radix = 16).toChar().toString() } val json = GALLERY_JSON_REGEX.find(input.body!!.string())!!.groupValues[1].replace(
UNICODE_ESCAPE_REGEX
) { it.groupValues[1].toInt(radix = 16).toChar().toString() }
val obj = JsonParser.parseString(json).asJsonObject val obj = JsonParser.parseString(json).asJsonObject
with(metadata) { with(metadata) {
@@ -195,164 +94,13 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
tags.clear() tags.clear()
}?.forEach { }?.forEach {
if (it.first != null && it.second != null) { if (it.first != null && it.second != null) {
tags.add(RaisedTag(it.first!!, it.second!!, TAG_TYPE_DEFAULT)) tags.add(RaisedTag(it.first!!, it.second!!, if (it.first == "category") RaisedSearchMetadata.TAG_TYPE_VIRTUAL else NHentaiSearchMetadata.TAG_TYPE_DEFAULT))
} }
} }
} }
} }
private fun getOrLoadMetadata(mangaId: Long?, nhId: Long) = getOrLoadMetadata(mangaId) { override fun toString() = "${delegate.name} (${lang.toUpperCase()})"
client.newCall(nhGet(baseUrl + NHentaiSearchMetadata.nhIdToPath(nhId)))
.asObservableSuccess()
.toSingle()
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 1f
}
)
)
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = getOrLoadMetadata(chapter.mangaId, NHentaiSearchMetadata.nhUrlToId(chapter.url)).map { metadata ->
if (metadata.mediaId == null) {
emptyList()
} else {
metadata.pageImageTypes.mapIndexed { index, s ->
val imageUrl = imageUrlFromType(metadata.mediaId!!, index + 1, s)
Page(index, imageUrl!!, imageUrl)
}
}
}.toObservable()
override fun fetchImageUrl(page: Page) = Observable.just(page.imageUrl!!)!!
private fun imageUrlFromType(mediaId: String, page: Int, t: String) = NHentaiSearchMetadata.typeToExtension(t)?.let {
"https://i.nhentai.net/galleries/$mediaId/$page.$it"
}
override fun chapterListParse(response: Response): List<SChapter> {
throw NotImplementedError("Unused method called!")
}
override fun pageListParse(response: Response): List<Page> {
throw NotImplementedError("Unused method called!")
}
override fun imageUrlParse(response: Response): String {
throw NotImplementedError("Unused method called!")
}
private fun combineQuery(filters: FilterList): String {
val stringBuilder = StringBuilder()
val advSearch = filters.filterIsInstance<AdvSearchEntryFilter>().flatMap { filter ->
val splitState = filter.state.split(",").map(String::trim).filterNot(String::isBlank)
splitState.map {
AdvSearchEntry(filter.name, it.removePrefix("-"), it.startsWith("-"))
}
}
advSearch.forEach { entry ->
if (entry.exclude) stringBuilder.append("-")
stringBuilder.append("${entry.name}:")
stringBuilder.append(entry.text)
stringBuilder.append(" ")
}
val langFilter = filters.filterIsInstance<FilterLang>().firstOrNull()
if (langFilter != null) {
val language = SOURCE_LANG_LIST.first { it.first == langFilter.values[langFilter.state] }.second
if (!language.isBlank()) {
stringBuilder.append("language:$language")
}
}
return stringBuilder.toString()
}
data class AdvSearchEntry(val name: String, val text: String, val exclude: Boolean)
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TagFilter(),
CategoryFilter(),
GroupFilter(),
ArtistFilter(),
ParodyFilter(),
CharactersFilter(),
Filter.Header("Uploaded valid units are h, d, w, m, y."),
Filter.Header("example: (>20d)"),
UploadedFilter(),
Filter.Separator(),
SortFilter(),
Filter.Header("Sort is ignored if favorites only"),
FavoriteFilter(),
FilterLang()
)
class TagFilter : AdvSearchEntryFilter("Tags")
class CategoryFilter : AdvSearchEntryFilter("Categories")
class GroupFilter : AdvSearchEntryFilter("Groups")
class ArtistFilter : AdvSearchEntryFilter("Artists")
class ParodyFilter : AdvSearchEntryFilter("Parodies")
class CharactersFilter : AdvSearchEntryFilter("Characters")
class UploadedFilter : AdvSearchEntryFilter("Uploaded")
open class AdvSearchEntryFilter(name: String) : Filter.Text(name)
private class FavoriteFilter : Filter.CheckBox("Show favorites only", false)
// language filtering
private class FilterLang : Filter.Select<String>("Language", SOURCE_LANG_LIST.map { it.first }.toTypedArray())
private class SortFilter : UriPartFilter(
"Sort By",
arrayOf(
Pair("Popular: All Time", "popular"),
Pair("Popular: Week", "popular-week"),
Pair("Popular: Today", "popular-today"),
Pair("Recent", "date")
)
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private val appName by lazy {
context.getString(R.string.app_name)
}
private fun nhGet(url: String, tag: Any? = null) = GET(url)
.newBuilder()
.header(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/56.0.2924.87 " +
"Safari/537.36 " +
"$appName/${BuildConfig.VERSION_CODE}"
)
.tag(tag).build()
override val id = NHENTAI_SOURCE_ID
override val lang = "all"
override val name = "nhentai"
override val baseUrl = NHentaiSearchMetadata.BASE_URL
override val supportsLatest = true
// === URL IMPORT STUFF
override val matchingHosts = listOf( override val matchingHosts = listOf(
"nhentai.net" "nhentai.net"
@@ -366,16 +114,14 @@ class NHentai(context: Context) : HttpSource(), LewdSource<NHentaiSearchMetadata
return "$baseUrl/g/${uri.pathSegments[1]}/" return "$baseUrl/g/${uri.pathSegments[1]}/"
} }
override fun getDescriptionAdapter(controller: MangaController): NHentaiDescriptionAdapter {
return NHentaiDescriptionAdapter(controller)
}
companion object { companion object {
const val otherId = 7309872737163460316L
private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);") private val GALLERY_JSON_REGEX = Regex(".parse\\(\"(.*)\"\\);")
private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})") private val UNICODE_ESCAPE_REGEX = Regex("\\\\u([0-9a-fA-F]{4})")
private const val REVERSE_PARAM = "TEH_REVERSE"
private val SOURCE_LANG_LIST = listOf(
Pair("All", ""),
Pair("English", "english"),
Pair("Japanese", "japanese"),
Pair("Chinese", "chinese")
)
} }
} }
+26 -251
View File
@@ -1,153 +1,48 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
import exh.metadata.metadata.PervEdenLang
import exh.metadata.metadata.PervEdenSearchMetadata import exh.metadata.metadata.PervEdenSearchMetadata
import exh.metadata.metadata.PervEdenSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.util.UriFilter import exh.source.DelegatedHttpSource
import exh.util.UriGroup import exh.ui.metadata.adapters.PervEdenDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
// TODO Transform into delegated source class PervEden(delegate: HttpSource, val context: Context) :
class PervEden(override val id: Long, val pvLang: PervEdenLang) : DelegatedHttpSource(delegate),
ParsedHttpSource(),
LewdSource<PervEdenSearchMetadata, Document>, LewdSource<PervEdenSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
/**
* The class of the metadata used by this source
*/
override val metaClass = PervEdenSearchMetadata::class override val metaClass = PervEdenSearchMetadata::class
override val lang = delegate.lang
override val supportsLatest = true
override val name = "Perv Eden"
override val baseUrl = "http://www.perveden.com"
override val lang = pvLang.name
override fun popularMangaSelector() = "#topManga > ul > li"
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = "http:" + element.select(".hottestImage > img").attr("data-src")
val titleElement = element.getElementsByClass("hottestInfo").first().child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text()
return manga
}
override fun popularMangaNextPageSelector(): String? = null
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun searchMangaSelector() = "#mangaList > tbody > tr"
override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
val titleElement = element.child(0).child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text().trim()
return manga
}
override fun searchMangaNextPageSelector() = ".next"
override fun popularMangaRequest(page: Int): Request {
val urlLang = if (lang == "en") {
"eng"
} else {
"it"
}
return GET("$baseUrl/$urlLang/")
}
override fun latestUpdatesSelector() = ".newsManga"
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val header = element.getElementsByClass("manga_tooltop_header").first()
val titleElement = header.child(0)
manga.url = titleElement.attr("href")
manga.title = titleElement.text().trim()
manga.thumbnail_url = "https:" + header.parent().selectFirst(".mangaImage img").attr("tmpsrc")
return manga
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
return MangasPage(mangas, mangas.isNotEmpty())
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = Uri.parse("$baseUrl/$lang/$lang-directory/").buildUpon()
uri.appendQueryParameter("page", page.toString())
uri.appendQueryParameter("title", query)
filters.forEach {
if (it is UriFilter) it.addToUri(uri)
}
return GET(uri.toString())
}
override fun latestUpdatesNextPageSelector(): String? {
throw NotImplementedError("Unused method called!")
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
parseToManga(manga, it.asJsoup()).andThen( parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
Observable.just(
manga.apply {
initialized = true
}
)
)
} }
} }
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: PervEdenSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
url = Uri.parse(input.location()).path url = Uri.parse(input.location()).path
@@ -181,12 +76,18 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
"Artist" -> { "Artist" -> {
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
artist = it.text() artist = it.text()
tags += RaisedTag("artist", it.text().toLowerCase(), TAG_TYPE_VIRTUAL) tags += RaisedTag(
"artist", it.text().toLowerCase(),
RaisedSearchMetadata.TAG_TYPE_VIRTUAL
)
} }
} }
"Genres" -> { "Genres" -> {
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
tags += RaisedTag(null, it.text().toLowerCase(), TAG_TYPE_DEFAULT) tags += RaisedTag(
null, it.text().toLowerCase(),
PervEdenSearchMetadata.TAG_TYPE_DEFAULT
)
} }
} }
"Type" -> { "Type" -> {
@@ -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 val matchingHosts = listOf("www.perveden.com")
override fun matchesUri(uri: Uri): Boolean { override fun matchesUri(uri: Uri): Boolean {
return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (pvLang) { return super.matchesUri(uri) && uri.pathSegments.firstOrNull()?.toLowerCase() == when (lang) {
PervEdenLang.en -> "en-manga" "en" -> "en-manga"
PervEdenLang.it -> "it-manga" "it" -> "it-manga"
else -> false
} }
} }
@@ -357,9 +134,7 @@ class PervEden(override val id: Long, val pvLang: PervEdenLang) :
return newUri.toString() return newUri.toString()
} }
companion object { override fun getDescriptionAdapter(controller: MangaController): PervEdenDescriptionAdapter {
val DATE_FORMAT = SimpleDateFormat("MMM d, yyyy", Locale.US).apply { return PervEdenDescriptionAdapter(controller)
timeZone = TimeZone.getTimeZone("GMT")
}
} }
} }
@@ -1,250 +1,38 @@
package eu.kanade.tachiyomi.source.online.english package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri import android.net.Uri
import com.kizitonwose.time.hours
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.EIGHTMUSES_SOURCE_ID
import exh.metadata.metadata.EightMusesSearchMetadata import exh.metadata.metadata.EightMusesSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.util.CachedField import exh.source.DelegatedHttpSource
import exh.util.NakedTrie import exh.ui.metadata.adapters.EightMusesDescriptionAdapter
import exh.util.await
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import hu.akarnokd.rxjava.interop.RxJavaInterop
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.withContext
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers
typealias SiteMap = NakedTrie<Unit> class EightMuses(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
class EightMuses :
HttpSource(),
LewdSource<EightMusesSearchMetadata, Document>, LewdSource<EightMusesSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val id = EIGHTMUSES_SOURCE_ID
/**
* Name of the source.
*/
override val name = "8muses"
/**
* Whether the source has support for latest updates.
*/
override val supportsLatest = true
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang: String = "en"
override val metaClass = EightMusesSearchMetadata::class override val metaClass = EightMusesSearchMetadata::class
override val lang = "en"
/** // Support direct URL importing
* Base url of the website without the trailing slash, like: http://mysite.com override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
*/ urlImportFetchSearchManga(context, query) {
override val baseUrl = EightMusesSearchMetadata.BASE_URL super.fetchSearchManga(page, query, filters)
private val siteMapCache = CachedField<SiteMap>(1.hours.inMilliseconds.longValue)
override val client: OkHttpClient
get() = network.cloudflareClient
private suspend fun obtainSiteMap() = siteMapCache.obtain {
withContext(Dispatchers.IO) {
val result = client.newCall(eightMusesGet("$baseUrl/sitemap/1.xml"))
.asObservableSuccess()
.toSingle()
.await(Schedulers.io())
.body!!.string()
val parsed = Jsoup.parse(result)
val seen = NakedTrie<Unit>()
parsed.getElementsByTag("loc").forEach { item ->
seen[item.text().substring(22)] = Unit
}
seen
}
}
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;")
.add("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8")
.add("Referer", "https://www.8muses.com")
.add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36")
}
private fun eightMusesGet(url: String): Request {
return GET(url, headers = headersBuilder().build())
}
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int) = eightMusesGet("$baseUrl/comics/$page")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val urlBuilder = if (!query.isBlank()) {
"$baseUrl/search".toHttpUrlOrNull()!!
.newBuilder()
.addQueryParameter("q", query)
} else {
"$baseUrl/comics".toHttpUrlOrNull()!!
.newBuilder()
} }
urlBuilder.addQueryParameter("page", page.toString())
filters.filterIsInstance<SortFilter>().map {
it.addToUri(urlBuilder)
}
return eightMusesGet(urlBuilder.toString())
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int) = eightMusesGet("$baseUrl/comics/lastupdate?page=$page")
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): MangasPage {
throw UnsupportedOperationException("Should not be called!")
}
// override fun fetchLatestUpdates(page: Int) = fetchListing(latestUpdatesRequest(page), false)
override fun fetchLatestUpdates(page: Int) = fetchListing(popularMangaRequest(page), false)
override fun fetchPopularManga(page: Int) = fetchListing(popularMangaRequest(page), false) // TODO Dig
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return urlImportFetchSearchManga(query) {
fetchListing(searchMangaRequest(page, query, filters), false)
}
}
private fun fetchListing(request: Request, dig: Boolean): Observable<MangasPage> {
return client.newCall(request)
.asObservableSuccess()
.flatMapSingle { response ->
RxJavaInterop.toV1Single(
GlobalScope.async(Dispatchers.IO) {
parseResultsPage(response, dig)
}.asSingle(GlobalScope.coroutineContext)
)
}
}
private suspend fun parseResultsPage(response: Response, dig: Boolean): MangasPage {
val doc = response.asJsoup()
val contents = parseSelf(doc)
val onLastPage = doc.selectFirst(".current:nth-last-child(2)") != null
return MangasPage(
if (dig) {
contents.albums.flatMap {
val href = it.attr("href")
val splitHref = href.split('/')
obtainSiteMap().subMap(href).filter {
it.key.split('/').size - splitHref.size == 1
}.map { (key, _) ->
SManga.create().apply {
url = key
title = key.substringAfterLast('/').replace('-', ' ')
}
}
}
} else {
contents.albums.map {
SManga.create().apply {
url = it.attr("href")
title = it.select(".title-text").text()
thumbnail_url = baseUrl + it.select(".lazyload").attr("data-src")
}
}
},
!onLastPage
)
}
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response): SManga {
throw UnsupportedOperationException("Should not be called!")
}
/**
* Returns an observable with the updated details for a manga. Normally it's not needed to
* override this method.
*
* @param manga the manga to be updated.
*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga)) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess() .asObservableSuccess()
@@ -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>) data class SelfContents(val albums: List<Element>, val images: List<Element>)
private fun parseSelf(doc: Document): SelfContents { private fun parseSelf(doc: Document): SelfContents {
@@ -306,22 +54,6 @@ class EightMuses :
return SelfContents(selfAlbums, selfImages) return SelfContents(selfAlbums, selfImages)
} }
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
val contents = parseSelf(response.asJsoup())
return contents.images.mapIndexed { index, element ->
Page(
index,
element.attr("href"),
"$baseUrl/image/fl" + element.select(".lazyload").attr("data-src").substring(9)
)
}
}
override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) { override fun parseIntoMetadata(metadata: EightMusesSearchMetadata, input: Document) {
with(metadata) { with(metadata) {
path = Uri.parse(input.location()).pathSegments path = Uri.parse(input.location()).pathSegments
@@ -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( override val matchingHosts = listOf(
"www.8muses.com", "www.8muses.com",
"comics.8muses.com",
"8muses.com" "8muses.com"
) )
@@ -396,4 +97,8 @@ class EightMuses :
} }
return "/comics/album/${path.joinToString("/")}" 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 package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri 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.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.HBROWSE_SOURCE_ID
import exh.metadata.metadata.HBrowseSearchMetadata import exh.metadata.metadata.HBrowseSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.search.Namespace import exh.source.DelegatedHttpSource
import exh.search.SearchEngine import exh.ui.metadata.adapters.HBrowseDescriptionAdapter
import exh.search.Text
import exh.util.await
import exh.util.dropBlank
import exh.util.urlImportFetchSearchManga 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.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable 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 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() override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
.add("Cookie", BASE_COOKIES) return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
private val clientWithoutCookies = client.newBuilder() .flatMap {
.cookieJar(CookieJar.NO_COOKIES) parseToManga(manga, it.asJsoup()).andThen(Observable.just(manga))
.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")
} }
}
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) { override fun parseIntoMetadata(metadata: HBrowseSearchMetadata, input: Document) {
val tables = parseIntoTables(input) val tables = parseIntoTables(input)
with(metadata) { with(metadata) {
hbId = Uri.parse(input.location()).pathSegments.first().toLong() hbUrl = input.location().removePrefix("$baseUrl/thumbnails")
hbId = hbUrl!!.removePrefix("/").substringBefore("/").toLong()
tags.clear() tags.clear()
(tables[""]!! + tables["categories"]!!).forEach { (k, v) -> ((tables[""] ?: error("")) + (tables["categories"] ?: error(""))).forEach { (k, v) ->
when (val lowercaseNs = k.toLowerCase()) { when (val lowercaseNs = k.toLowerCase()) {
"title" -> title = v.text() "title" -> title = v.text()
"length" -> length = v.text().substringBefore(" ").toInt() "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>> { private fun parseIntoTables(doc: Document): Map<String, Map<String, Element>> {
return doc.select("#main > .listTable").map { ele -> return doc.select("#main > .listTable").map { ele ->
val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: "" val tableName = ele.previousElementSibling()?.text()?.toLowerCase() ?: ""
@@ -378,602 +76,16 @@ class HBrowse : HttpSource(), LewdSource<HBrowseSearchMetadata, Document>, UrlIm
}.toMap() }.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( override val matchingHosts = listOf(
"www.hbrowse.com", "www.hbrowse.com",
"hbrowse.com" "hbrowse.com"
) )
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
return "$baseUrl/${uri.pathSegments.first()}" return "/${uri.pathSegments.first()}/c00001/"
} }
companion object { override fun getDescriptionAdapter(controller: MangaController): HBrowseDescriptionAdapter {
private val PAGE_LIST_REGEX = Regex("list *= *(\\[.*]);") return HBrowseDescriptionAdapter(controller)
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")
} }
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.english package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@@ -8,18 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.HentaiCafeSearchMetadata import exh.metadata.metadata.HentaiCafeSearchMetadata
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class HentaiCafe(delegate: HttpSource) : class HentaiCafe(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<HentaiCafeSearchMetadata, Document>, LewdSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
@@ -34,7 +37,7 @@ class HentaiCafe(delegate: HttpSource) :
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
@@ -110,4 +113,8 @@ class HentaiCafe(delegate: HttpSource) :
"https://hentai.cafe/$lcFirstPathSegment" "https://hentai.cafe/$lcFirstPathSegment"
} }
} }
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
return HentaiCafeDescriptionAdapter(controller)
}
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online.english package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@@ -8,17 +9,20 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.PururinSearchMetadata import exh.metadata.metadata.PururinSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.PururinDescriptionAdapter
import exh.util.dropBlank import exh.util.dropBlank
import exh.util.trimAll import exh.util.trimAll
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class Pururin(delegate: HttpSource) : class Pururin(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<PururinSearchMetadata, Document>, LewdSource<PururinSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
@@ -38,7 +42,7 @@ class Pururin(delegate: HttpSource) :
"$baseUrl/gallery/$trimmedIdQuery/-" "$baseUrl/gallery/$trimmedIdQuery/-"
} else query } else query
return urlImportFetchSearchManga(newQuery) { return urlImportFetchSearchManga(context, newQuery) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
} }
@@ -88,10 +92,11 @@ class Pururin(delegate: HttpSource) :
else -> { else -> {
value.select("a").forEach { link -> value.select("a").forEach { link ->
val searchUrl = Uri.parse(link.attr("href")) val searchUrl = Uri.parse(link.attr("href"))
val namespace = searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2]
tags += RaisedTag( tags += RaisedTag(
searchUrl.pathSegments[searchUrl.pathSegments.lastIndex - 2], namespace,
searchUrl.lastPathSegment!!.substringBefore("."), 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? { override fun mapUrlToMangaUrl(uri: Uri): String? {
return "${PururinSearchMetadata.BASE_URL}/gallery/${uri.pathSegments[1]}/${uri.lastPathSegment}" 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 package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LewdSource import eu.kanade.tachiyomi.source.online.LewdSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import exh.metadata.metadata.TsuminoSearchMetadata import exh.metadata.metadata.TsuminoSearchMetadata
import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT import exh.metadata.metadata.TsuminoSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
import exh.util.dropBlank
import exh.util.trimAll
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
class Tsumino(delegate: HttpSource) : class Tsumino(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>, LewdSource<TsuminoSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
@@ -27,13 +33,13 @@ class Tsumino(delegate: HttpSource) :
override val lang = "en" override val lang = "en"
// Support direct URL importing // Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
override fun mapUrlToMangaUrl(uri: Uri): String? { 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") { if (lcFirstPathSegment != "read" && lcFirstPathSegment != "book" && lcFirstPathSegment != "entry") {
return null return null
} }
@@ -57,9 +63,12 @@ class Tsumino(delegate: HttpSource) :
title = it.trim() title = it.trim()
} }
input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { input.getElementById("Artist")?.children()?.first()?.text()?.trim()?.let { artistString ->
tags.add(RaisedTag("artist", it, TAG_TYPE_VIRTUAL)) artistString.split("|").trimAll().dropBlank().forEach {
artist = it 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 { input.getElementById("Uploader")?.children()?.first()?.text()?.trim()?.let {
@@ -76,6 +85,12 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Rating")?.text()?.let { input.getElementById("Rating")?.text()?.let {
ratingString = it.trim() 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 { input.getElementById("Category")?.children()?.first()?.text()?.let {
@@ -85,18 +100,19 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Collection")?.children()?.first()?.text()?.let { input.getElementById("Collection")?.children()?.first()?.text()?.let {
collection = it.trim() collection = it.trim()
tags.add(RaisedTag("collection", it, TAG_TYPE_DEFAULT))
} }
input.getElementById("Group")?.children()?.first()?.text()?.let { input.getElementById("Group")?.children()?.first()?.text()?.let {
group = it.trim() group = it.trim()
tags.add(RaisedTag("group", it, TAG_TYPE_VIRTUAL)) tags.add(RaisedTag("group", it, TAG_TYPE_DEFAULT))
} }
val newParody = mutableListOf<String>() val newParody = mutableListOf<String>()
input.getElementById("Parody")?.children()?.forEach { input.getElementById("Parody")?.children()?.forEach {
val entry = it.text().trim() val entry = it.text().trim()
newParody.add(entry) newParody.add(entry)
tags.add(RaisedTag("parody", entry, TAG_TYPE_VIRTUAL)) tags.add(RaisedTag("parody", entry, TAG_TYPE_DEFAULT))
} }
parody = newParody parody = newParody
@@ -104,14 +120,14 @@ class Tsumino(delegate: HttpSource) :
input.getElementById("Character")?.children()?.forEach { input.getElementById("Character")?.children()?.forEach {
val entry = it.text().trim() val entry = it.text().trim()
newCharacter.add(entry) newCharacter.add(entry)
tags.add(RaisedTag("character", entry, TAG_TYPE_VIRTUAL)) tags.add(RaisedTag("character", entry, TAG_TYPE_DEFAULT))
} }
character = newCharacter character = newCharacter
input.getElementById("Tag")?.children()?.let { input.getElementById("Tag")?.children()?.let { tagElements ->
tags.addAll( tags.addAll(
it.map { tagElements.map {
RaisedTag(null, it.text().trim(), TAG_TYPE_DEFAULT) RaisedTag("tags", it.text().trim(), TAG_TYPE_DEFAULT)
} }
) )
} }
@@ -125,6 +141,12 @@ class Tsumino(delegate: HttpSource) :
companion object { companion object {
val TM_DATE_FORMAT = SimpleDateFormat("yyyy MMM dd", Locale.US) 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 return null
} }
fun setTitle() { fun setTitle(title: String? = null) {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) { if (parentController is BaseController<*> && parentController.getTitle() != null) {
@@ -83,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
parentController = parentController.parentController parentController = parentController.parentController
} }
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() (activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
} }
private fun Controller.instance(): String { 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) return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler(duration))
.popChangeHandler(FadeChangeHandler()) .popChangeHandler(FadeChangeHandler(duration))
} }
@@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
/** /**
* Dismiss the dialog and pop this controller * Dismiss the dialog and pop this controller
*/ */
fun dismissDialog() { private fun dismissDialog() {
if (dismissed) { if (dismissed) {
return 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) { override fun configureTabs(tabs: TabLayout) {
with(tabs) { with(tabs) {
tabGravity = TabLayout.GRAVITY_FILL 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.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import androidx.core.view.marginBottom
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
@@ -22,11 +23,10 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio
for (i in 0 until childCount - 1) { for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
val holder = parent.getChildViewHolder(child) val holder = parent.getChildViewHolder(child)
if (holder is SourceHolder && if (holder is SourceListItem &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceListItem
) { ) {
val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + child.marginBottom
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight val bottom = top + divider.intrinsicHeight
val left = parent.paddingStart + holder.margin val left = parent.paddingStart + holder.margin
val right = parent.width - parent.paddingEnd - 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.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.SourceDividerItemDecoration
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -75,7 +76,7 @@ open class ExtensionController :
// Create recycler and set adapter. // Create recycler and set adapter.
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter binding.recycler.adapter = adapter
binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context)) binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
adapter?.fastScroller = binding.fastScroller adapter?.fastScroller = binding.fastScroller
} }
@@ -129,6 +130,9 @@ open class ExtensionController :
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE searchView.maxWidth = Int.MAX_VALUE
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
searchItem.expandActionView() searchItem.expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
@@ -142,9 +146,6 @@ open class ExtensionController :
drawExtensions() drawExtensions()
} }
.launchIn(scope) .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 { 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.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.ui.browse.SourceListItem
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.extension_card_item.card 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) : class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter), BaseFlexibleViewHolder(view, adapter),
SourceListItem,
SlicedHolder { SlicedHolder {
override val slice = Slice(card).apply { override val slice = Slice(card).apply {
@@ -45,12 +47,15 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
version.text = extension.versionName version.text = extension.versionName
lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context) lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
warning.text = when { warning.text = when {
extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase() extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted)
extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase() extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete)
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase() extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial)
extension is Extension.Installed && extension.isRedundant -> itemView.context.getString(R.string.ext_redundant).toUpperCase() // SY -->
else -> null 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) GlideApp.with(itemView.context).clear(image)
if (extension is Extension.Available) { if (extension is Extension.Available) {
@@ -91,12 +96,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
setText(R.string.ext_update) setText(R.string.ext_update)
} }
else -> { else -> {
// SY -->
if (extension.sources.any { it is ConfigurableSource }) { if (extension.sources.any { it is ConfigurableSource }) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
text = context.getString(R.string.action_settings) + "+" text = context.getString(R.string.action_settings) + "+"
} else { } else {
setText(R.string.action_settings) setText(R.string.action_settings)
} }
// SY <--
} }
} }
} else if (extension is Extension.Untrusted) { } else if (extension is Extension.Untrusted) {
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@@ -55,20 +56,22 @@ open class ExtensionPresenter(
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> { private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()
val showNsfwExtensions = preferences.allowNsfwSource().get() != PreferenceValues.NsfwAllowance.BLOCKED
val (installed, untrusted, available) = tuple val (installed, untrusted, available) = tuple
val items = mutableListOf<ExtensionItem>() val items = mutableListOf<ExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate }.sortedBy { it.pkgName } val updatesSorted = installed.filter { it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedBy { it.pkgName }
val installedSorted = installed.filter { !it.hasUpdate }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName })) val installedSorted = installed.filter { !it.hasUpdate && (showNsfwExtensions || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete /* SY --> */ && !it.isRedundant /* SY <-- */ }, { it.pkgName }))
val untrustedSorted = untrusted.sortedBy { it.pkgName } val untrustedSorted = untrusted.sortedBy { it.pkgName }
val availableSorted = available val availableSorted = available
// Filter out already installed extensions and disabled languages // Filter out already installed extensions and disabled languages
.filter { avail -> .filter { avail ->
installed.none { it.pkgName == avail.pkgName } && installed.none { it.pkgName == avail.pkgName } &&
untrusted.none { it.pkgName == avail.pkgName } && untrusted.none { it.pkgName == avail.pkgName } &&
(avail.lang in activeLangs || avail.lang == "all") (avail.lang in activeLangs || avail.lang == "all") &&
(showNsfwExtensions || !avail.isNsfw)
} }
.sortedBy { it.pkgName } .sortedBy { it.pkgName }
@@ -2,7 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@@ -23,14 +26,16 @@ import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange import eu.kanade.tachiyomi.util.preference.onChange
@@ -45,7 +50,7 @@ import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle), NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
NoToolbarElevationController { ToolbarLiftOnScrollController {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@@ -180,6 +185,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
when (item.itemId) { when (item.itemId) {
R.id.action_enable_all -> toggleAllSources(true) R.id.action_enable_all -> toggleAllSources(true)
R.id.action_disable_all -> toggleAllSources(false) R.id.action_disable_all -> toggleAllSources(false)
R.id.action_open_in_settings -> openInSettings()
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@@ -193,15 +199,18 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
} }
private fun toggleSource(source: Source, enable: Boolean) { 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( private fun openInSettings() {
if (enable) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
current - source.id.toString() data = Uri.fromParts("package", presenter.pkgName, null)
} else { }
current + source.id.toString() startActivity(intent)
}
)
} }
private fun Source.isEnabled(): Boolean { private fun Source.isEnabled(): Boolean {
@@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.extension.details
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -42,6 +42,7 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
binding.extensionTitle.text = extension.name binding.extensionTitle.text = extension.name
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName) binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context)) binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
binding.extensionNsfw.isVisible = extension.isNsfw
binding.extensionPkg.text = extension.pkgName binding.extensionPkg.text = extension.pkgName
binding.extensionUninstallButton.clicks() binding.extensionUninstallButton.clicks()
@@ -49,18 +50,18 @@ class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPrese
.launchIn(scope) .launchIn(scope)
if (extension.isObsolete) { if (extension.isObsolete) {
binding.extensionWarningBanner.visible() binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message) binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
} }
if (extension.isUnofficial) { if (extension.isUnofficial) {
binding.extensionWarningBanner.visible() binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message) binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
} }
// SY --> // SY -->
if (extension.isRedundant) { if (extension.isRedundant) {
binding.extensionWarningBanner.visible() binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.redundant_extension_message) binding.extensionWarningBanner.setText(R.string.redundant_extension_message)
} }
// SY <-- // 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.LatestAdapter
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestPresenter
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaAllInOneController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -72,11 +71,7 @@ open class LatestController :
*/ */
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
// Open MangaController. // Open MangaController.
if (presenter.preferences.eh_useNewMangaInterface().get()) { parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
parentController?.router?.pushController(MangaAllInOneController(manga, true).withFadeTransaction())
} else {
parentController?.router?.pushController(MangaController(manga, true).withFadeTransaction())
}
} }
/** /**
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.ui.browse.latest package eu.kanade.tachiyomi.ui.browse.latest
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.LatestAdapter
import eu.kanade.tachiyomi.util.view.gone import kotlinx.android.synthetic.main.latest_controller_card.no_results_found
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.android.synthetic.main.latest_controller_card.progress 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.recycler
import kotlinx.android.synthetic.main.latest_controller_card.source_card import kotlinx.android.synthetic.main.latest_controller_card.source_card
@@ -61,16 +61,16 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
when { when {
results == null -> { results == null -> {
progress.visible() progress.isVisible = true
showHolder() showResultsHolder()
} }
results.isEmpty() -> { results.isEmpty() -> {
progress.gone() progress.isVisible = false
hideHolder() showNoResults()
} }
else -> { else -> {
progress.gone() progress.isVisible = false
showHolder() showResultsHolder()
} }
} }
if (results !== lastBoundResults) { if (results !== lastBoundResults) {
@@ -105,13 +105,13 @@ class LatestHolder(view: View, val adapter: LatestAdapter) :
return null return null
} }
private fun showHolder() { private fun showResultsHolder() {
title_wrapper.visible() no_results_found.isVisible = false
source_card.visible() source_card.isVisible = true
} }
private fun hideHolder() { private fun showNoResults() {
title_wrapper.gone() no_results_found.isVisible = true
source_card.gone() source_card.isVisible = false
} }
} }

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