Compare commits

..

128 Commits

Author SHA1 Message Date
Jobobby04 332a631b6c Release 1.3.1
Validate Gradle Wrapper / Validation (push) Successful in 13s
2020-09-15 13:21:37 -04:00
Jobobby04 70bef08ed6 Disable tag sorting for release 2020-09-15 13:03:15 -04:00
Jobobby04 de05f88d5f Fix Mangadex 2 factor auth
Fix Backups with merge manga breaking(I think)
Tweak preload settings, make the max 20, defaults to 10
Add tag based sorting
2020-09-15 13:00:40 -04:00
Jay 89427ff37e Send manga/chapter/page details when sharing a chapter page
(cherry picked from commit 32aea55f424d77e78be50f288996fe04053db009)
2020-09-14 21:15:40 -04:00
Jobobby04 63617f3079 Release 1.3.0
Validate Gradle Wrapper / Validation (push) Failing after 23s
2020-09-14 20:24:48 -04:00
Jobobby04 0f9f7ffc28 Cleanup 2020-09-14 14:36:56 -04:00
Jobobby04 9aab9d4ca4 Fix autofill exclusion 2020-09-14 14:06:48 -04:00
Jobobby04 92ae67630c Hide dedupe by priority for release 2020-09-14 12:47:54 -04:00
arkon 02b90000f0 Hide parental controls section for release
(cherry picked from commit 76c795d0d0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
2020-09-14 12:42:39 -04:00
arkon 6ed5d858aa Fix Kotlinter name typo
(cherry picked from commit b20bced3ca)
2020-09-14 12:40:58 -04:00
arkon 83bc059573 Fix Chinese plurals
(cherry picked from commit 4f2da9a78f)
2020-09-14 12:40:47 -04:00
Jozef Hollý ad39af55d6 Translated using Weblate (Bulgarian) (#3646)
Currently translated at 99.4% (574 of 577 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (French)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (German)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Italian)

Currently translated at 98.9% (570 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Yakut)

Currently translated at 87.3% (503 of 576 strings)

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

Translated using Weblate (Bulgarian)

Currently translated at 96.7% (557 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Vietnamese)

Currently translated at 86.2% (497 of 576 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 95.4% (550 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Yakut)

Currently translated at 85.2% (491 of 576 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (574 of 576 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 99.3% (572 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Latvian)

Currently translated at 33.6% (194 of 576 strings)

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

Translated using Weblate (Arabic)

Currently translated at 91.8% (529 of 576 strings)

Translated using Weblate (Latvian)

Currently translated at 33.3% (192 of 576 strings)

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

Translated using Weblate (Latvian)

Currently translated at 33.3% (192 of 576 strings)

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

Translated using Weblate (French)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.7% (569 of 576 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Croatian)

Currently translated at 99.4% (573 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 0.5% (3 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn_BD/

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (563 of 576 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (576 of 576 strings)

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

Translated using Weblate (Turkish)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 99.1% (571 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Polish)

Currently translated at 96.8% (558 of 576 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali)

Currently translated at 57.6% (332 of 576 strings)

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

Translated using Weblate (Bengali)

Currently translated at 57.6% (332 of 576 strings)

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

Added translation using Weblate (Bengali (Bangladesh))

Translated using Weblate (Yakut)

Currently translated at 80.2% (462 of 576 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali)

Currently translated at 58.3% (336 of 576 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (565 of 576 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.9% (570 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.4% (521 of 576 strings)

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

Translated using Weblate (Malay)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (576 of 576 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (573 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Gabriel Lebis <gableb@hotmail.fr>
Co-authored-by: George <georgeramzy37@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: RealKC <mitrut.e.super@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Whod <whodizhod@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Strings

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Gabriel Lebis <gableb@hotmail.fr>
Co-authored-by: George <georgeramzy37@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: RealKC <mitrut.e.super@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Whod <whodizhod@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
(cherry picked from commit 8e0ba3650b)
2020-09-14 12:40:33 -04:00
arkon 8d5b2f40b3 Use Kolinter Gradle plugin for linting instead of ktlint directly
(cherry picked from commit 76f6fe4601)
2020-09-13 23:08:52 -04:00
arkon 49bee1af91 Update to Preivews new update checker, while keeping the SY repos and versions
(cherry picked from commit ca1373f36b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdateChecker.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubService.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt
2020-09-13 23:03:22 -04:00
Jobobby04 a48508a98e reader light theme chapters bottom sheet fixes 2020-09-13 22:52:11 -04:00
arkon 4e3288b2af Use background color for some lists
(cherry picked from commit c0789cd6ba)
2020-09-13 22:51:46 -04:00
arkon 0f16150613 Replace deprecated system window insets usage
(cherry picked from commit af47103707)
2020-09-13 22:24:06 -04:00
arkon 6dd7491ffe Remove list dividers
(cherry picked from commit c466baaa25)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterDividerItemDecoration.kt
2020-09-13 22:24:06 -04:00
Jobobby04 02e6eaae12 Cleanup and fix a few compiler warnings 2020-09-13 22:24:03 -04:00
Jobobby04 73523dbff8 Remove Manga Plus delegation as it is not needed with the new extension update 2020-09-13 18:54:38 -04:00
Jobobby04 58a503814d Update firebase crashlytics 2020-09-13 18:46:14 -04:00
Jobobby04 7a834ea9f4 Mangadex tweaks, fix global update crashing sometimes 2020-09-13 18:45:57 -04:00
Jobobby04 dcca19e6b8 Undo Reds manga title changes 2020-09-12 19:21:54 -04:00
arkon 210fd000b3 Update OkHttp and Conscrypt
(cherry picked from commit 670294a427)
2020-09-12 13:51:55 -04:00
arkon 3d6f8ddd13 Update to Kotlin 1.4.10
(cherry picked from commit 21ddae6a86)
2020-09-12 13:51:55 -04:00
Andreas E a4578611d7 Always show missing chapter warning if there are missing chapters (#3755)
* Always show missing chapter warning if there are missing chapters

* Change function parameter names

(cherry picked from commit 9f260c3513)
2020-09-12 13:51:55 -04:00
Jobobby04 bb6932ff80 Mangadex follows, tell browse there are no more pages 2020-09-12 13:51:54 -04:00
AbdullahM0hamed 8dce9a674b Fix title on light theme (#101) 2020-09-12 13:51:19 -04:00
Jobobby04 b93298c411 Add MangaDex only implementation of Mangadex Follows list
Add login dialog that pops up whenever you are not logged in when trying to browse MangaDex
Remove attempts at porting over chapter read history from older galleries to new ones
Disable latest for ExHentai, it was browse without buttons anyway
2020-09-11 23:12:13 -04:00
Jobobby04 8928aa77eb Disable logging thread info, it wasnt very useful and made the log difficult to read 2020-09-10 20:27:49 -04:00
jobobby04 a6e6fa0099 Merge pull request #96 from AbdullahM0hamed/patch-1
Remove navigationBarColor from Black-Red theme
2020-09-10 18:43:46 -04:00
AbdullahM0hamed c23edd5b72 Remove navigationBarColor from Black-Red theme 2020-09-10 23:42:14 +01:00
AbdullahM0hamed c8a4ec37e0 Add Black-Red theme (#95)
* Appveyor

* stuff

* resolve conflict

* Let's try this again

* try again

* More fixing

* remove appveyor

* revert build.gradle

* Revert "revert build.gradle"

This reverts commit feaaa78157ffe8d6d6af7d6d63a74bc14b92f584.

* Undo line change

* Update build.gradle

* Update MainActivity.kt

Co-authored-by: AbdullahM0hamed<AbdullahM0hamed@users.noreply.github.com>
2020-09-10 17:06:49 -04:00
Jobobby04 f62d277894 Opps, forgot to push these changes 2020-09-08 12:04:53 -04:00
Jobobby04 b7efc21ea9 Update pt-rBR translation(curtsy of 0k//lux)
Change some gallery references to manga
2020-09-08 11:57:10 -04:00
Jobobby04 238b2d108d Cleanup 2020-09-06 22:43:58 -04:00
Jobobby04 bdaf0f7492 Fix exclusion in library search 2020-09-06 22:43:45 -04:00
Jobobby04 ced8dc750a Cleanup 2020-09-06 21:30:36 -04:00
Jobobby04 035fb9e755 Add biometric lock times 2020-09-06 21:30:22 -04:00
Jobobby04 86defec57c Fix latest + browse page crash when source returns null 2020-09-06 20:05:10 -04:00
Jobobby04 f20e5d864d Move from kizitonwose time to kotlin duration 2020-09-06 16:24:43 -04:00
Jobobby04 d83f938e07 Add a option to allow local source to read hidden folders 2020-09-06 00:31:08 -04:00
Jobobby04 b4e73cb1eb Upgrade the cleanup downloads to the new J2k version 2020-09-05 19:43:12 -04:00
arkon 604c7c703a Fix text alignment in transition view when no more chapters available
(cherry picked from commit b55d394a1f)
2020-09-05 19:20:53 -04:00
Andreas E 20021dbf54 Add spacing on top of sources/extensions/migrate lists (#3751)
(cherry picked from commit 5e2e177aa9)
2020-09-05 19:20:42 -04:00
arkon 281fb9c67b Refactor common chapter transition views into separate view
(cherry picked from commit 86e59977de)
2020-09-05 19:19:49 -04:00
arkon 7579bb026f Localize "No chapters found" error
(cherry picked from commit 66baf01e43)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2020-09-05 19:19:37 -04:00
Andreas E 61fb836be4 Add missing chapter warning (#3745)
* Add missing chapter warning

* Flip calculation instead of flipping variables

* Change logic

* Change tint based on reader theme

* Add missing chapter warning to WebtoonTransitionHolder

* Add chapter warning between current/finished and prev/next

* Fix mix up of TextViews

* Fix review comments

(cherry picked from commit 7a33e198dc)
2020-09-05 19:11:20 -04:00
scb261 d83361dfe3 Change sources sort to case-insensitive (#3743)
(cherry picked from commit 4b493ebbaf)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceFilterController.kt
2020-09-05 19:11:02 -04:00
arkon a5a79c1127 Remove unused string, fix improperly formatted Slovak string
(cherry picked from commit 565e8cf00b)
2020-09-05 19:03:58 -04:00
arkon 2f0f938d5e Update Conscrypt
(cherry picked from commit 738a3999b4)

# Conflicts:
#	app/build.gradle
2020-09-05 19:03:52 -04:00
arkon 750a6c3d11 Move share manga button to toolbar menu
(cherry picked from commit 8bedc8f456)
2020-09-05 19:02:48 -04:00
arkon b65305c73e Update dependencies
(cherry picked from commit d9000f6fd1)
2020-09-05 19:02:38 -04:00
Jobobby04 85ef1031b5 Display read progress on read chapters for E/Exhentai manga 2020-09-05 19:02:09 -04:00
Jobobby04 69c530dc34 Fix migrate to source with most chapters 2020-09-05 18:19:04 -04:00
Jobobby04 ea620a8c74 Probably fix previously read exh chapters affect updated gallery chapters 2020-09-05 18:18:39 -04:00
Jobobby04 1fdbae5bf8 Cleanup 2020-09-05 18:17:48 -04:00
Jobobby04 a1d54880c3 Merged manga implementation, man this took forever to make and bugfix, its not even done 2020-09-05 18:17:33 -04:00
Jobobby04 d21a652944 Remake the merged database, has support for future features 2020-09-04 17:31:35 -04:00
Jobobby04 444d346874 Probably make previously read exh chapters affect updated gallery chapters when loading the new version in the source 2020-08-25 02:09:15 -04:00
Jobobby04 41b8786415 Cleanup some mangatype logs 2020-08-25 02:07:40 -04:00
Jobobby04 d97805e38b Respect manga chapter order for reader chapter list, as well as fix page progress updating 2020-08-25 00:12:07 -04:00
Jobobby04 eafe3a62e4 Fix the bottom sheet and remove the bookmark button at the top of the reader 2020-08-24 19:25:47 -04:00
Jobobby04 0a502fcf31 Cleanup merged source code so I can modify it easier later on 2020-08-24 17:28:14 -04:00
Jobobby04 80960d87f2 Cleanup 2020-08-24 17:25:10 -04:00
Jobobby04 166aebdf25 Grab started filter from J2k 2020-08-23 21:59:43 -04:00
Jobobby04 b8836b9b6f Update firebase 2020-08-23 21:57:06 -04:00
Jobobby04 1d70f0b1dd Lint 2020-08-22 22:13:14 -04:00
Jobobby04 1240cc5232 Reader bottom sheet transparency, as well as a half fix for the fullscreen reader bug 2020-08-22 22:12:59 -04:00
Jobobby04 0abee585fc Some more cleanup to Save as CBZ 2020-08-22 18:41:42 -04:00
arkon 4870bb153d Filter out hidden directories for local source (closes #3706)
(cherry picked from commit fe7c7e72f5)
2020-08-22 18:19:33 -04:00
Jobobby04 f2b90bd772 Cleanup 2020-08-22 18:19:33 -04:00
arkon d77c65b515 Clean up X-Requested-With change
This only really affects the initial request, subsequent requests may still use the package name.

(cherry picked from commit 9920ff617b)
2020-08-22 18:17:48 -04:00
armangido 5004e2d62c Update WebViewActivity.kt (#3617)
This code added is for some extension that blocks tachiyomi, by tricking it that it was sent by a android browser, nothing major changes,

(cherry picked from commit 3f1355c413)
2020-08-22 18:17:37 -04:00
arkon 6e4a0ca1ea Update ActionMode styling
(cherry picked from commit 4929e66ecc)
2020-08-22 18:17:29 -04:00
arkon 883ffaa815 AndroidX dependency updates
(cherry picked from commit 4c31e3fc5f)
2020-08-22 18:17:08 -04:00
Jobobby04 3967a569c4 Cleanup Save as CBZ 2020-08-22 18:12:32 -04:00
Jobobby04 79e4e3d2a0 Add chapters bottom sheet for reader 2020-08-22 17:37:42 -04:00
Jobobby04 4b12e977c0 Cleanup some code 2020-08-22 17:37:42 -04:00
Fahad1998 4333999b85 Add Save As CBZ (#84)
Co-authored-by: Fahad1998 <f1998>
2020-08-22 17:37:03 -04:00
Fahad1998 fae290cf22 data saver fix (#85)
Co-authored-by: Fahad1998 <f1998>
2020-08-22 17:35:50 -04:00
Jobobby04 9ecf83f842 Delegate mangaplus so that it doesnt crash the app, it is literally just a copy of the extension with no extra features, but at least the app doesnt crash when you use it now 2020-08-21 16:27:01 -04:00
Jobobby04 f594962731 Cleanup 2020-08-21 15:32:42 -04:00
Jobobby04 048eecf655 Optimize and cleanup data saver 2020-08-21 15:32:29 -04:00
Fahad1998 90253f3bd4 Add Data Saver (#82)
Co-authored-by: Fahad1998 <f1998>
2020-08-21 13:50:08 -04:00
Jobobby04 0deb6f6b8d Finish some more advanced mangadex delegation features, more to come later 2020-08-20 20:50:37 -04:00
Jobobby04 294ade035e Rename LewdSource to MetadataSource to account for that Metadata will not always be for lewd sources 2020-08-19 18:45:56 -04:00
Jobobby04 64bb34b50d Delegate Mangadex (Part 1) 2020-08-19 18:20:05 -04:00
Jobobby04 9efb1482f9 Updated delegation system to account for custom headers 2020-08-19 18:19:22 -04:00
Jobobby04 aff15b3ee2 Remove library search log 2020-08-19 18:14:42 -04:00
Jobobby04 d86f3ffad8 Add a custom view change handler, makes it fade only one way 2020-08-19 02:08:58 -04:00
arkon 2a3eef0610 Don't enqueue bookmarked chapters for deletion (fixes #3691)
(cherry picked from commit 4c8665c9f0)
2020-08-18 22:32:06 -04:00
arkon 3b87111f22 Minor wording edit
(cherry picked from commit ba67781431)

# Conflicts:
#	app/src/main/res/values/strings.xml
2020-08-18 22:31:50 -04:00
arkon b654613345 Use core-ktx for bolding chapter transition text
(cherry picked from commit 4ef25c75b7)
2020-08-18 22:31:20 -04:00
arkon 136b25fb92 Dependency updates
(cherry picked from commit 3aafc671f8)

# Conflicts:
#	app/build.gradle
2020-08-18 22:31:00 -04:00
arkon f3875bda50 Update to Kotlin 1.4
(cherry picked from commit 967df6f7a3)

# Conflicts:
#	app/build.gradle
2020-08-18 22:29:54 -04:00
Jobobby04 c41148b465 Make animations smoother, add change handlers to bottom nav transactions, make animations go by their default value instead of a custom fast one 2020-08-18 22:23:39 -04:00
Jobobby04 eb0a1668f8 Made the ViewHeightAnimator duration configurable 2020-08-18 22:07:39 -04:00
Jobobby04 f7bc3e0a82 Cleanup some strings 2020-08-18 22:07:14 -04:00
Jobobby04 2e4def13e3 Add custom browese view, disabled by default and can be enabled in the settings
Signed-off-by: Jobobby04 <jobobby04@users.noreply.github.com>
2020-08-18 22:05:56 -04:00
Jobobby04 9e0e2db25d Add dynamic category library update upgrades 2020-08-18 18:55:00 -04:00
Jobobby04 20aa5b9aa1 Cleanup E-Hentai 2020-08-18 18:31:00 -04:00
Jobobby04 6ce4612aa7 Fix latest packages 2020-08-16 20:57:10 -04:00
Jobobby04 b51d147986 Cleanup 2020-08-16 20:47:09 -04:00
Jobobby04 bc896cf605 Make XLog display debug info in a debug build 2020-08-16 20:40:30 -04:00
Jobobby04 e48f274072 Updates and cleanup build.gradle 2020-08-15 23:01:16 -04:00
Jobobby04 a6b98e24dc Undo linting 2020-08-15 15:57:07 -04:00
Jobobby04 9a26a3e5a2 Revert "Slight gradle cleanup, plugin updates"
This reverts commit 08d11914af.
2020-08-14 18:54:34 -04:00
Jobobby04 eeb0f76cce Cleanup 2020-08-14 18:41:21 -04:00
Jobobby04 bcd36c8fad Add tap to move by page for continues vertical reader 2020-08-14 18:41:21 -04:00
Jobobby04 7f7b2901cb Convert autoscroll to a coroutine 2020-08-14 18:41:21 -04:00
Jobobby04 84b9b4db55 Cleanup 2020-08-14 18:41:21 -04:00
Jobobby04 a5c10dbf28 Add no title grid option 2020-08-14 18:41:21 -04:00
Jobobby04 5fee3ac05a Add a option to open the library bottom sheet in the library Settings 2020-08-14 18:41:21 -04:00
Jobobby04 d3603a664c Un-break XLog network logging, code contributed from Neko 2020-08-14 18:41:21 -04:00
Jobobby04 7ccaea0d72 Remove unused hitomi class 2020-08-14 18:41:20 -04:00
Jobobby04 921f7aad01 Make FAB disappear on image expand 2020-08-14 18:41:20 -04:00
Jobobby04 bc549c56d6 Automatic linting fixes 2020-08-14 18:41:20 -04:00
Jobobby04 b639e1e4d7 Delete my pull request tester in favor of previews 2020-08-14 18:41:13 -04:00
arkon 4f877820b2 Add PR build check action
(cherry picked from commit 19a7f37efa)
2020-08-14 18:41:13 -04:00
arkon c35e0a0c29 Unhide parental controls settings
(cherry picked from commit c3084ac43a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
2020-08-14 18:41:13 -04:00
TacoTheDank 08d11914af Slight gradle cleanup, plugin updates
(cherry picked from commit 159146e197)

# Conflicts:
#	app/build.gradle
#	build.gradle.kts
2020-08-14 18:41:13 -04:00
Jobobby04 309bd83730 Update gradle wrapper
(cherry picked from commit 67ddf4a5b8)
2020-08-14 18:40:55 -04:00
Taco 6b158cc864 Optimize images using ezgif (#3649) 2020-08-12 22:45:32 -04:00
Jobobby04 7d82be964c Add long click to copy on the special manga views 2020-08-12 22:34:20 -04:00
Jobobby04 525c3f84e4 Enable click to copy to clipboard in the more info 2020-08-12 22:32:13 -04:00
Jobobby04 ef37811020 Expand manga thumbnails 2020-08-12 21:10:39 -04:00
Jobobby04 0d033c7080 Cleanup some delegation code 2020-08-12 18:46:05 -04:00
444 changed files with 13098 additions and 2900 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.2.0) - I have updated to the latest version of the app (stable is v1.3.1)
- 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
+1 -1
View File
@@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.2.0) - I have updated to the latest version of the app (stable is v1.3.1)
- 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
+1 -1
View File
@@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v1.2.0) - I have updated to the latest version of the app (stable is v1.3.1)
- 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
@@ -0,0 +1,11 @@
name: Validate Gradle Wrapper
on: [push, pull_request]
jobs:
validation:
name: Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
+1
View File
@@ -1,5 +1,6 @@
name: Issue closer name: Issue closer
on: [issues] on: [issues]
jobs: jobs:
autoclose: autoclose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -1,23 +1,21 @@
name: Pull Request Checker name: Pull request build check
on: [pull_request]
on:
pull_request:
jobs: jobs:
apk: build:
name: Generate APK runs-on: ubuntu-latest
runs-on: ubuntu-18.04
steps: steps:
- uses: actions/checkout@v2 - name: Clone repo
- name: set up JDK 1.8 uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 1.8
- name: Get NDK - name: Install NDK
run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669" run: sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;21.0.6113669"
- name: Build Release APK - name: Build project
run: bash ./gradlew assembleDebug --stacktrace run: ./gradlew assembleDebug
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
+37 -55
View File
@@ -7,6 +7,7 @@ apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.github.zellius.shortcut-helper' apply plugin: 'com.github.zellius.shortcut-helper'
// Realm (EH) // Realm (EH)
apply plugin: 'realm-android' apply plugin: 'realm-android'
@@ -42,8 +43,8 @@ android {
minSdkVersion AndroidConfig.minSdk minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 6 versionCode 9
versionName "1.2.0" versionName "1.3.1"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -65,7 +66,6 @@ android {
debug { debug {
versionNameSuffix "-${getCommitCount()}" versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
ext.enableCrashlytics = false
} }
releaseTest { releaseTest {
applicationIdSuffix ".rt" applicationIdSuffix ".rt"
@@ -140,11 +140,11 @@ dependencies {
// 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-alpha02'
implementation 'androidx.biometric:biometric:1.0.1' implementation 'androidx.biometric:biometric:1.1.0-alpha02'
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-rc1' implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
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.core:core-ktx:1.4.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
@@ -152,20 +152,20 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
final lifecycle_version = '2.3.0-alpha06' final lifecycle_version = '2.3.0-alpha07'
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' final work_version = '2.5.0-alpha01'
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-alpha02' implementation 'com.google.android.material:material:1.3.0-alpha02'
standardImplementation 'com.google.firebase:firebase-core:17.4.4' standardImplementation 'com.google.firebase:firebase-core:17.5.0'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
@@ -174,14 +174,14 @@ 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.8.1' final okhttp_version = '4.9.0'
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.7.0' implementation 'com.squareup.okio:okio:2.8.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.5.1'
// REST // REST
final retrofit_version = '2.9.0' final retrofit_version = '2.9.0'
@@ -217,7 +217,7 @@ dependencies {
implementation 'io.requery:sqlite-android:3.32.2' implementation 'io.requery:sqlite-android:3.32.2'
// Preferences // Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.3.0' implementation 'com.github.tfcporciuncula:flow-preferences:1.3.1'
// Model View Presenter // Model View Presenter
final nucleus_version = '3.0.0' final nucleus_version = '3.0.0'
@@ -277,13 +277,12 @@ 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.3.0' // NOTE: REMEMBER TO UPDATE GRADLE PLUGIN
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version" implementation 'com.mikepenz:aboutlibraries:8.3.0'
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
// Tests // Tests
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:3.12.2' testImplementation 'org.assertj:assertj-core:3.16.1'
testImplementation 'org.mockito:mockito-core:1.10.19' testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4' final robolectric_version = '3.1.4'
@@ -291,54 +290,38 @@ dependencies {
testImplementation "org.robolectric:shadows-multidex:$robolectric_version" testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$robolectric_version" testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// SY for mangadex utils
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:1.0.0-RC"
final coroutines_version = '1.3.8' final coroutines_version = '1.3.9'
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-rx2:$coroutines_version"
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' // 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'
// Reprint (EH)
implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
implementation 'com.github.ajalt.reprint:rxjava:3.2.1@aar' // optional: the RxJava 1 interface
// Swirl (EH)
implementation 'com.mattprecious.swirl:swirl:1.2.0'
// RxJava 2 interop for Realm (EH)
implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
// Firebase (EH) // Firebase (EH)
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.google.firebase:firebase-analytics-ktx:17.5.0'
implementation 'com.google.firebase:firebase-crashlytics-ktx:17.2.1'
// Better logging (EH) // Better logging (EH)
implementation 'com.elvishew:xlog:1.6.1' implementation 'com.elvishew:xlog:1.6.1'
// Time utils (EH)
def typed_time_version = '1.0.2'
implementation "com.github.kizitonwose.time:time:$typed_time_version"
implementation "com.github.kizitonwose.time:time-android:$typed_time_version"
// Debug utils (EH) // Debug utils (EH)
debugImplementation 'com.ms-square:debugoverlay:1.1.3' final def debug_overlay_version = '1.1.3'
releaseTestImplementation 'com.ms-square:debugoverlay:1.1.3' debugImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
releaseImplementation 'com.ms-square:debugoverlay-no-op:1.1.3' releaseTestImplementation "com.ms-square:debugoverlay:$debug_overlay_version"
testImplementation 'com.ms-square:debugoverlay-no-op:1.1.3' releaseImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
testImplementation "com.ms-square:debugoverlay-no-op:$debug_overlay_version"
// Humanize (EH) // Humanize (EH) used for E-Hentai updater statistics
implementation 'com.github.mfornos:humanize-slim:1.2.2' implementation 'com.github.mfornos:humanize-slim:1.2.2'
// RatingBar (SY) // RatingBar (SY)
@@ -346,7 +329,7 @@ dependencies {
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.5.1'
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version" implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
@@ -355,16 +338,15 @@ dependencies {
implementation "io.noties.markwon:image:$markwon_version" implementation "io.noties.markwon:image:$markwon_version"
implementation "io.noties.markwon:linkify:$markwon_version" implementation "io.noties.markwon:linkify:$markwon_version"
implementation 'com.google.guava:guava:27.0.1-android' implementation 'com.google.guava:guava:29.0-android'
} }
buildscript { buildscript {
ext.kotlin_version = '1.3.72'
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
} }
} }
@@ -374,7 +356,7 @@ repositories {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
tasks.withType(AbstractKotlinCompile).all { tasks.withType(AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
} }
// Duplicating Hebrew string assets due to some locale code issues on different devices // Duplicating Hebrew string assets due to some locale code issues on different devices
@@ -384,10 +366,10 @@ task copyResources(type: Copy) {
include '**/*' include '**/*'
} }
preBuild.dependsOn(ktlintFormat, copyResources) preBuild.dependsOn(formatKotlin, copyResources)
if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) { if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Debug")) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
// Firebase (EH) // Firebase Crashlytics
apply plugin: 'io.fabric' apply plugin: 'com.google.firebase.crashlytics'
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

+2 -2
View File
@@ -282,7 +282,7 @@
android:scheme="https" /> android:scheme="https" />
<!-- MangaDex --> <!-- MangaDex -->
<!-- <data <data
android:host="mangadex.org" android:host="mangadex.org"
android:pathPattern="\/(title|manga)\/" android:pathPattern="\/(title|manga)\/"
android:scheme="http" /> android:scheme="http" />
@@ -297,7 +297,7 @@
<data <data
android:host="www.mangadex.org" android:host="www.mangadex.org"
android:pathPattern="\/(title|manga)\/" android:pathPattern="\/(title|manga)\/"
android:scheme="https" />--> android:scheme="https" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
+44 -17
View File
@@ -23,7 +23,9 @@ import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.gms.common.GooglePlayServicesRepairableException import com.google.android.gms.common.GooglePlayServicesRepairableException
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.kizitonwose.time.days import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay 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
@@ -34,13 +36,9 @@ import exh.debug.DebugToggles
import exh.log.CrashlyticsPrinter import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
import exh.log.EHLogLevel import exh.log.EHLogLevel
import exh.syDebugVersion
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import java.io.File
import java.security.NoSuchAlgorithmException
import java.security.Security
import javax.net.ssl.SSLContext
import kotlin.concurrent.thread
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
@@ -49,13 +47,23 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.io.File
import java.security.NoSuchAlgorithmException
import java.security.Security
import javax.net.ssl.SSLContext
import kotlin.concurrent.thread
import kotlin.time.ExperimentalTime
import kotlin.time.days
open class App : Application(), LifecycleObserver { open class App : Application(), LifecycleObserver {
private lateinit var firebaseAnalytics: FirebaseAnalytics
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupExhLogging() // EXH logging setupExhLogging() // EXH logging
if (!BuildConfig.DEBUG) addAnalytics()
workaroundAndroid7BrokenSSL() workaroundAndroid7BrokenSSL()
@@ -79,7 +87,6 @@ open class App : Application(), LifecycleObserver {
setupNotificationChannels() setupNotificationChannels()
Realm.init(this) 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)
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {
setupDebugOverlay() setupDebugOverlay()
} }
@@ -119,6 +126,13 @@ open class App : Application(), LifecycleObserver {
} }
} }
private fun addAnalytics() {
firebaseAnalytics = Firebase.analytics
if (syDebugVersion != "0") {
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion)
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@Suppress("unused") @Suppress("unused")
fun onAppBackgrounded() { fun onAppBackgrounded() {
@@ -159,15 +173,14 @@ open class App : Application(), LifecycleObserver {
private fun setupExhLogging() { private fun setupExhLogging() {
EHLogLevel.init(this) EHLogLevel.init(this)
val logLevel = if (EHLogLevel.shouldLog(EHLogLevel.EXTRA)) { val logLevel = when {
LogLevel.ALL EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL
} else { BuildConfig.DEBUG -> LogLevel.DEBUG
LogLevel.WARN else -> LogLevel.WARN
} }
val logConfig = LogConfiguration.Builder() val logConfig = LogConfiguration.Builder()
.logLevel(logLevel) .logLevel(logLevel)
.t()
.st(2) .st(2)
.nb() .nb()
.build() .build()
@@ -180,14 +193,17 @@ open class App : Application(), LifecycleObserver {
"logs" "logs"
) )
@OptIn(ExperimentalTime::class)
printers += FilePrinter printers += FilePrinter
.Builder(logFolder.absolutePath) .Builder(logFolder.absolutePath)
.fileNameGenerator(object : DateFileNameGenerator() { .fileNameGenerator(
override fun generateFileName(logLevel: Int, timestamp: Long): String { object : DateFileNameGenerator() {
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}" override fun generateFileName(logLevel: Int, timestamp: Long): String {
return super.generateFileName(logLevel, timestamp) + "-${BuildConfig.BUILD_TYPE}.log"
}
} }
}) )
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.inMilliseconds.longValue)) .cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
.backupStrategy(NeverBackupStrategy()) .backupStrategy(NeverBackupStrategy())
.build() .build()
@@ -202,6 +218,17 @@ open class App : Application(), LifecycleObserver {
) )
XLog.d("Application booting...") XLog.d("Application booting...")
XLog.nst().d(
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
"Preview build: $syDebugVersion\n" +
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
"Android build ID: ${Build.DISPLAY}\n" +
"Device brand: ${Build.BRAND}\n" +
"Device manufacturer: ${Build.MANUFACTURER}\n" +
"Device name: ${Build.DEVICE}\n" +
"Device model: ${Build.MODEL}\n" +
"Device product name: ${Build.PRODUCT}"
)
} }
// EXH // EXH
@@ -8,9 +8,9 @@ import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
@@ -36,8 +36,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: preferences.backupInterval().get() val interval = prefInterval ?: preferences.backupInterval().get()
if (interval > 0) { if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), TimeUnit.HOURS, interval.toLong(),
10, TimeUnit.MINUTES TimeUnit.HOURS,
10,
TimeUnit.MINUTES
) )
.addTag(TAG) .addTag(TAG)
.build() .build()
@@ -17,6 +17,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
@@ -32,6 +33,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.backup.models.DHistory
@@ -39,6 +41,7 @@ import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MergedMangaReferenceTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.CategoryImpl import eu.kanade.tachiyomi.data.database.models.CategoryImpl
@@ -57,17 +60,23 @@ import eu.kanade.tachiyomi.source.LocalSource
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.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import exh.EXHSavedSearch import exh.EXHSavedSearch
import exh.MERGED_SOURCE_ID
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import java.lang.RuntimeException import exh.merged.sql.models.MergedMangaReference
import kotlin.math.max import exh.util.asObservable
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
import java.lang.RuntimeException
import kotlin.math.max
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
@@ -106,6 +115,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build()) .registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build()) .registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
// SY -->
.registerTypeAdapter<MergedMangaReference>(MergedMangaReferenceTypeAdapter.build())
// SY <--
.create() .create()
else -> throw Exception("Json version unknown") else -> throw Exception("Json version unknown")
} }
@@ -129,15 +141,21 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Create extension ID/name mapping // Create extension ID/name mapping
val extensionEntries = JsonArray() val extensionEntries = JsonArray()
// Merged Manga References
val mergedMangaReferenceEntries = JsonArray()
// Add value's to root // Add value's to root
root[Backup.VERSION] = CURRENT_VERSION root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries root[EXTENSIONS] = extensionEntries
// SY -->
root[MERGEDMANGAREFERENCES] = mergedMangaReferenceEntries
// SY <--
databaseHelper.inTransaction { databaseHelper.inTransaction {
// Get manga from database // Get manga from database
val mangas = getFavoriteManga() val mangas = getFavoriteManga().filterNot { it.source == MERGED_SOURCE_ID } /* SY --> */ + getMergedManga() /* SY <-- */
val extensions: MutableSet<String> = mutableSetOf() val extensions: MutableSet<String> = mutableSetOf()
@@ -163,6 +181,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// SY --> // SY -->
root[SAVEDSEARCHES] = root[SAVEDSEARCHES] =
Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***") Injekt.get<PreferencesHelper>().eh_savedSearches().get().joinToString(separator = "***")
backupMergedMangaReferences(mergedMangaReferenceEntries)
// SY <-- // SY <--
} }
@@ -212,6 +232,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
} }
} }
// SY -->
private fun backupMergedMangaReferences(root: JsonArray) {
val mergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
mergedMangaReferences.forEach { root.add(parser.toJsonTree(it)) }
}
// SY <--
/** /**
* Backup the categories of library * Backup the categories of library
* *
@@ -317,29 +344,40 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> { fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> {
// SY --> // SY -->
return ( if (source is MergedSource) {
if (source is EHentai) { val syncedChapters = runBlocking { source.fetchChaptersAndSync(manga, false) }
source.fetchChapterList(manga, throttleManager::throttle) return syncedChapters.onEach { pair ->
} else {
source.fetchChapterList(manga)
}
).map {
if (it.last().chapter_number == -99F) {
chapters.forEach { chapter ->
chapter.name = "Chapter ${chapter.chapter_number} restored by dummy source"
}
syncChaptersWithSource(databaseHelper, chapters, manga, source)
} else {
syncChaptersWithSource(databaseHelper, it, manga, source)
}
}
// SY <--
.doOnNext { pair ->
if (pair.first.isNotEmpty()) { if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters) insertChapters(chapters)
} }
}.asObservable()
} else {
return (
if (source is EHentai) {
source.fetchChapterList(manga, throttleManager::throttle)
} else {
source.fetchChapterList(manga)
}
).map {
if (it.last().chapter_number == -99F) {
chapters.forEach { chapter ->
chapter.name =
"Chapter ${chapter.chapter_number} restored by dummy source"
}
syncChaptersWithSource(databaseHelper, chapters, manga, source)
} else {
syncChaptersWithSource(databaseHelper, it, manga, source)
}
} }
// SY <--
.doOnNext { pair ->
if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
}
}
} }
/** /**
@@ -584,6 +622,57 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
} }
preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet()) preferences.eh_savedSearches().set((otherSerialized + newSerialized).toSet())
} }
/**
* Restore the categories from Json
*
* @param jsonMergedMangaReferences array containing md manga references
*/
internal fun restoreMergedMangaReferences(jsonMergedMangaReferences: JsonArray) {
// Get merged manga references from file and from db
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
val backupMergedMangaReferences = parser.fromJson<List<MergedMangaReference>>(jsonMergedMangaReferences)
var lastMergeManga: Manga? = null
// Iterate over them
backupMergedMangaReferences.forEach { mergedMangaReference ->
// Used to know if the merged manga reference is already in the db
var found = false
for (dbMergedMangaReference in dbMergedMangaReferences) {
// If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference
// and do nothing
if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) {
mergedMangaReference.id = dbMergedMangaReference.id
mergedMangaReference.mergeId = dbMergedMangaReference.mergeId
mergedMangaReference.mangaId = dbMergedMangaReference.mangaId
found = true
break
}
}
// If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference
// Store the inserted id in the mergedMangaReference
if (!found) {
// Let the db assign the id
var mergedManga = if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga
if (mergedManga == null) {
mergedManga = Manga.create(MERGED_SOURCE_ID).apply {
url = mergedMangaReference.mergeUrl
title = context.getString(R.string.refresh_merge)
}
mergedManga.id = databaseHelper.insertManga(mergedManga).executeAsBlocking().insertedId()
}
val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
lastMergeManga = mergedManga
mergedMangaReference.mergeId = mergedManga.id
mergedMangaReference.mangaId = manga.id
mergedMangaReference.id = null
val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking()
mergedMangaReference.id = result.insertedId()
}
}
}
// SY <-- // SY <--
/** /**
@@ -602,6 +691,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
internal fun getFavoriteManga(): List<Manga> = internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
internal fun getMergedManga(): List<Manga> =
databaseHelper.getMergedMangas().executeAsBlocking()
/** /**
* Inserts manga and returns id * Inserts manga and returns id
* *
@@ -11,9 +11,9 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
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 uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.injectLazy
internal class BackupNotifier(private val context: Context) { internal class BackupNotifier(private val context: Context) {
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
import eu.kanade.tachiyomi.data.backup.models.Backup.MERGEDMANGAREFERENCES
import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES import eu.kanade.tachiyomi.data.backup.models.Backup.SAVEDSEARCHES
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
@@ -33,14 +34,11 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.EXHMigrations import exh.EXHMigrations
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -48,6 +46,10 @@ import kotlinx.coroutines.launch
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/** /**
* Restores backup from a JSON file. * Restores backup from a JSON file.
@@ -238,7 +240,7 @@ class BackupRestoreService : Service() {
} }
totalAmount = mangasJson.size() totalAmount = mangasJson.size()
restoreAmount = validManga.count() + 1 // +1 for categories restoreAmount = validManga.count() + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
skippedAmount = mangasJson.size() - validManga.count() skippedAmount = mangasJson.size() - validManga.count()
// SY <-- // SY <--
restoreProgress = 0 restoreProgress = 0
@@ -249,6 +251,8 @@ class BackupRestoreService : Service() {
// SY --> // SY -->
json.get(SAVEDSEARCHES)?.let { restoreSavedSearches(it) } json.get(SAVEDSEARCHES)?.let { restoreSavedSearches(it) }
json.get(MERGEDMANGAREFERENCES)?.let { restoreMergedMangaReferences(it) }
// SY <-- // SY <--
// Store source mapping for error messages // Store source mapping for error messages
@@ -288,6 +292,15 @@ class BackupRestoreService : Service() {
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches)) showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.saved_searches))
} }
private fun restoreMergedMangaReferences(mergedMangaReferencesJson: JsonElement) {
db.inTransaction {
backupManager.restoreMergedMangaReferences(mergedMangaReferencesJson.asJsonArray)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.merged_references))
}
// SY <-- // SY <--
private fun restoreManga(mangaJson: JsonObject) { private fun restoreManga(mangaJson: JsonObject) {
@@ -445,7 +458,12 @@ class BackupRestoreService : Service() {
return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */) return backupManager.restoreChapterFetchObservable(source, manga, chapters /* SY --> */, throttleManager /* SY <-- */)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") val errorMessage = if (it is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
it.message
}
errors.add(Date() to "${manga.title} - $errorMessage")
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
} }
@@ -19,6 +19,7 @@ object Backup {
const val VERSION = "version" const val VERSION = "version"
// SY --> // SY -->
const val SAVEDSEARCHES = "savedsearches" const val SAVEDSEARCHES = "savedsearches"
const val MERGEDMANGAREFERENCES = "mergedmangareferences"
// SY <-- // SY <--
fun getDefaultFilename(): String { fun getDefaultFilename(): String {
@@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import exh.merged.sql.models.MergedMangaReference
/**
* JSON Serializer used to write / read [MergedMangaReference] to / from json
*/
object MergedMangaReferenceTypeAdapter {
fun build(): TypeAdapter<MergedMangaReference> {
return typeAdapter {
write {
beginArray()
value(it.mangaUrl)
value(it.mergeUrl)
value(it.mangaSourceId)
value(it.chapterSortMode)
value(it.chapterPriority)
value(it.getChapterUpdates)
value(it.isInfoManga)
value(it.downloadChapters)
endArray()
}
read {
beginArray()
MergedMangaReference(
id = null,
mangaUrl = nextString(),
mergeUrl = nextString(),
mangaSourceId = nextLong(),
chapterSortMode = nextInt(),
chapterPriority = nextInt(),
getChapterUpdates = nextBoolean(),
isInfoManga = nextBoolean(),
downloadChapters = nextBoolean(),
mangaId = null,
mergeId = null
)
}
}
}
}
@@ -10,8 +10,6 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import java.io.File
import java.io.IOException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -22,6 +20,8 @@ import okio.buffer
import okio.sink import okio.sink
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/** /**
* Class used to create chapter cache * Class used to create chapter cache
@@ -21,6 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
import eu.kanade.tachiyomi.data.database.queries.MangaQueries import eu.kanade.tachiyomi.data.database.queries.MangaQueries
import eu.kanade.tachiyomi.data.database.queries.TrackQueries import eu.kanade.tachiyomi.data.database.queries.TrackQueries
import exh.merged.sql.mappers.MergedMangaTypeMapping
import exh.merged.sql.models.MergedMangaReference
import exh.merged.sql.queries.MergedQueries
import exh.metadata.sql.mappers.SearchMetadataTypeMapping import exh.metadata.sql.mappers.SearchMetadataTypeMapping
import exh.metadata.sql.mappers.SearchTagTypeMapping import exh.metadata.sql.mappers.SearchTagTypeMapping
import exh.metadata.sql.mappers.SearchTitleTypeMapping import exh.metadata.sql.mappers.SearchTitleTypeMapping
@@ -36,7 +39,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) : open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* EXH --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ { MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
@@ -51,11 +54,12 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(Category::class.java, CategoryTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping()) .addTypeMapping(History::class.java, HistoryTypeMapping())
// EXH --> // SY -->
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping()) .addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping()) .addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
// EXH <-- .addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
// SY <--
.build() .build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
@@ -7,8 +7,8 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.HistoryTable import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.MergedTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable import eu.kanade.tachiyomi.data.database.tables.TrackTable
import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
@@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = /* SY --> */ 3 /* SY <-- */ const val DATABASE_VERSION = /* SY --> */ 4 /* SY <-- */
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -34,14 +34,12 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(CategoryTable.createTableQuery) execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery) execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery) execSQL(HistoryTable.createTableQuery)
// EXH --> // SY -->
execSQL(SearchMetadataTable.createTableQuery) execSQL(SearchMetadataTable.createTableQuery)
execSQL(SearchTagTable.createTableQuery) execSQL(SearchTagTable.createTableQuery)
execSQL(SearchTitleTable.createTableQuery) execSQL(SearchTitleTable.createTableQuery)
// EXH <--
// AZ -->
execSQL(MergedTable.createTableQuery) execSQL(MergedTable.createTableQuery)
// AZ <-- // SY <--
// DB indexes // DB indexes
execSQL(MangaTable.createUrlIndexQuery) execSQL(MangaTable.createUrlIndexQuery)
@@ -49,17 +47,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(ChapterTable.createMangaIdIndexQuery) execSQL(ChapterTable.createMangaIdIndexQuery)
execSQL(ChapterTable.createUnreadChaptersIndexQuery) execSQL(ChapterTable.createUnreadChaptersIndexQuery)
execSQL(HistoryTable.createChapterIdIndexQuery) execSQL(HistoryTable.createChapterIdIndexQuery)
// EXH --> // SY -->
db.execSQL(SearchMetadataTable.createUploaderIndexQuery) execSQL(SearchMetadataTable.createUploaderIndexQuery)
db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery) execSQL(SearchMetadataTable.createIndexedExtraIndexQuery)
db.execSQL(SearchTagTable.createMangaIdIndexQuery) execSQL(SearchTagTable.createMangaIdIndexQuery)
db.execSQL(SearchTagTable.createNamespaceNameIndexQuery) execSQL(SearchTagTable.createNamespaceNameIndexQuery)
db.execSQL(SearchTitleTable.createMangaIdIndexQuery) execSQL(SearchTitleTable.createMangaIdIndexQuery)
db.execSQL(SearchTitleTable.createTitleIndexQuery) execSQL(SearchTitleTable.createTitleIndexQuery)
// EXH <--
// AZ -->
execSQL(MergedTable.createIndexQuery) execSQL(MergedTable.createIndexQuery)
// AZ <-- // SY <--
} }
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -70,6 +66,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(MangaTable.addDateAdded) db.execSQL(MangaTable.addDateAdded)
db.execSQL(MangaTable.backfillDateAdded) db.execSQL(MangaTable.backfillDateAdded)
} }
if (oldVersion < 12) {
db.execSQL(MergedTable.dropTableQuery)
db.execSQL(MergedTable.createTableQuery)
db.execSQL(MergedTable.createIndexQuery)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {
@@ -5,4 +5,8 @@ class LibraryManga : MangaImpl() {
var unread: Int = 0 var unread: Int = 0
var category: Int = 0 var category: Int = 0
// SY -->
var read: Int = 0
// SY <--
} }
@@ -15,9 +15,9 @@ import java.util.Date
interface ChapterQueries : DbProvider { interface ChapterQueries : DbProvider {
// SY --> // SY -->
fun getChapters(manga: Manga) = getChaptersByMangaId(manga.id) fun getChapters(manga: Manga) = getChapters(manga.id)
fun getChaptersByMangaId(mangaId: Long?) = db.get() fun getChapters(mangaId: Long?) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
.withQuery( .withQuery(
Query.builder() Query.builder()
@@ -27,15 +27,6 @@ interface ChapterQueries : DbProvider {
.build() .build()
) )
.prepare() .prepare()
fun getChaptersByMergedMangaId(mangaId: Long) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
RawQuery.builder()
.query(getMergedChaptersQuery(mangaId))
.build()
)
.prepare()
// SY <-- // SY <--
fun getRecentChapters(date: Date) = db.get() fun getRecentChapters(date: Date) = db.get()
@@ -94,6 +85,17 @@ interface ChapterQueries : DbProvider {
.build() .build()
) )
.prepare() .prepare()
fun getChaptersReadByUrls(urls: List<String>) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
.whereArgs(urls.joinToString { "\"$it\"" })
.build()
)
.prepare()
// SY <-- // SY <--
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare() fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
@@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
interface MangaQueries : DbProvider { interface MangaQueries : DbProvider {
@@ -77,15 +78,6 @@ interface MangaQueries : DbProvider {
.prepare() .prepare()
// SY --> // SY -->
fun getMergedMangas(id: Long) = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getMergedMangaQuery(id))
.build()
)
.prepare()
fun updateMangaInfo(manga: Manga) = db.put() fun updateMangaInfo(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaInfoPutResolver()) .withPutResolver(MangaInfoPutResolver())
@@ -139,7 +131,7 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?") .where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})")
.whereArgs(0) .whereArgs(0)
.build() .build()
) )
@@ -1,21 +1,48 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import exh.MERGED_SOURCE_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
import eu.kanade.tachiyomi.data.database.tables.MergedTable as Merged import exh.merged.sql.tables.MergedTable as Merged
// SY --> // SY -->
/** /**
* Query to get the manga merged into a merged manga * Query to get the manga merged into a merged manga
*/ */
fun getMergedMangaQuery(id: Long) = fun getMergedMangaQuery() =
""" """
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ( FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
) AS M
JOIN ${Manga.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
"""
/**
* Query to get all the manga that are merged into other manga
*/
fun getAllMergedMangaQuery() =
"""
SELECT ${Manga.TABLE}.*
FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE}
) AS M
JOIN ${Manga.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
"""
/**
* Query to get the manga merged into a merged manga using the Url
*/
fun getMergedMangaFromUrlQuery() =
"""
SELECT ${Manga.TABLE}.*
FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_URL} = ?
) AS M ) AS M
JOIN ${Manga.TABLE} JOIN ${Manga.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
@@ -24,16 +51,15 @@ fun getMergedMangaQuery(id: Long) =
/** /**
* Query to get the chapters of all manga in a merged manga * Query to get the chapters of all manga in a merged manga
*/ */
fun getMergedChaptersQuery(id: Long) = fun getMergedChaptersQuery() =
""" """
SELECT ${Chapter.TABLE}.* SELECT ${Chapter.TABLE}.*
FROM ( FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE $(Merged.COL_MERGE_ID} = $id SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ?
) AS M ) AS M
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = M.${Merged.COL_MANGA_ID}
""" """
// SY <--
/** /**
* Query to get the manga from the library, with their categories and unread count. * Query to get the manga from the library, with their categories and unread count.
@@ -42,23 +68,55 @@ val libraryQuery =
""" """
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0 WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS C ) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 LEFT JOIN (
GROUP BY ${Manga.COL_ID} SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS R
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
UNION
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread
FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 0
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read
FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_READ} = 1
GROUP BY ${Merged.TABLE}.${Merged.COL_MERGE_ID}
) AS R
ON ${Manga.TABLE}.${Manga.COL_ID} = R.${Merged.COL_MERGE_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} = $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE} ORDER BY ${Manga.COL_TITLE}
) AS M ) AS M
LEFT JOIN ( LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC SELECT * FROM ${MangaCategory.TABLE}
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} ) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID};
""" """
// SY <--
/** /**
* Query to get the recent chapters of manga from the library up to a date. * Query to get the recent chapters of manga from the library up to a date.
@@ -18,6 +18,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
mapBaseFromCursor(manga, cursor) mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD)) manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY)) manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
// SY -->
manga.read = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_READ))
// SY <--
return manga return manga
} }
@@ -38,6 +38,10 @@ object MangaTable {
const val COL_UNREAD = "unread" const val COL_UNREAD = "unread"
// SY ->>
const val COL_READ = "read"
// SY <--
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
const val COL_COVER_LAST_MODIFIED = "cover_last_modified" const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
@@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables
object MergedTable {
const val TABLE = "merged"
const val COL_MERGE_ID = "mergeID"
const val COL_MANGA_ID = "mangaID"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_MERGE_ID INTEGER NOT NULL,
$COL_MANGA_ID INTEGER NOT NULL
)"""
val createIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_MERGE_ID}_index ON $TABLE($COL_MERGE_ID)"
}
@@ -7,10 +7,10 @@ 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.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
/** /**
* Cache where we dump the downloads directory from the filesystem. This class is needed because * Cache where we dump the downloads directory from the filesystem. This class is needed because
@@ -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.getValidChapterDirNames(chapter).any { it in mangaDir.files } return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files || "$it.cbz" in mangaDir.files }
} }
} }
return false return false
@@ -145,7 +145,7 @@ class DownloadCache(
mangaDirs.values.forEach { mangaDir -> mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles() val chapterDirs = mangaDir.dir.listFiles()
.orEmpty() .orEmpty()
.mapNotNull { it.name } .mapNotNull { it.name?.replace(".cbz", "") }
.toHashSet() .toHashSet()
mangaDir.files = chapterDirs mangaDir.files = chapterDirs
@@ -196,6 +196,8 @@ class DownloadCache(
provider.getValidChapterDirNames(chapter).forEach { provider.getValidChapterDirNames(chapter).forEach {
if (it in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= it mangaDir.files -= it
} else if ("$it.cbz" in mangaDir.files) {
mangaDir.files -= "$it.cbz"
} }
} }
} }
@@ -226,6 +228,8 @@ class DownloadCache(
provider.getValidChapterDirNames(chapter).forEach { provider.getValidChapterDirNames(chapter).forEach {
if (it in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= it mangaDir.files -= it
} else if ("$it.cbz" in mangaDir.files) {
mangaDir.files -= "$it.cbz"
} }
} }
} }
@@ -198,14 +198,10 @@ class DownloadManager(/* SY private */ val context: Context) {
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
* @param source the source of the chapters. * @param source the source of the chapters.
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
queue.remove(chapters) val filteredChapters = getChaptersToDelete(chapters)
val filteredChapters = if (!preferences.removeBookmarkedChapters()) { queue.remove(filteredChapters)
chapters.filterNot { it.bookmark }
} else {
chapters
}
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source) val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() } chapterDirs.forEach { it.delete() }
@@ -213,9 +209,18 @@ class DownloadManager(/* SY private */ val context: Context) {
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()
} }
return filteredChapters
} }
// SY --> // SY -->
/**
* return the list of all manga folders
*/
fun getMangaFolders(source: Source): List<UniFile> {
return provider.findSourceDir(source)?.listFiles()?.toList() ?: emptyList()
}
/** /**
* Deletes the directories of chapters that were read or have no match * Deletes the directories of chapters that were read or have no match
* *
@@ -223,19 +228,39 @@ class DownloadManager(/* SY private */ val context: Context) {
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
* @param source the source of the chapters. * @param source the source of the chapters.
*/ */
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source): Int { fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int {
var cleaned = 0 var cleaned = 0
if (removeNonFavorite && !manga.favorite) {
val mangaFolder = provider.getMangaDir(manga, source)
cleaned += 1 + (mangaFolder.listFiles()?.size ?: 0)
mangaFolder.delete()
cache.removeManga(manga)
return cleaned
}
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
cleaned += filesWithNoChapter.size cleaned += filesWithNoChapter.size
cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga) cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga)
filesWithNoChapter.forEach { it.delete() } filesWithNoChapter.forEach { it.delete() }
val readChapters = allChapters.filter { it.read }
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source) if (removeRead) {
readChapterDirs.forEach { it.delete() } val readChapters = allChapters.filter { it.read }
cleaned += readChapterDirs.size val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
cache.removeChapters(readChapters, manga) readChapterDirs.forEach { it.delete() }
cleaned += readChapterDirs.size
cache.removeChapters(readChapters, manga)
}
if (cache.getDownloadCount(manga) == 0) { if (cache.getDownloadCount(manga) == 0) {
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty val mangaFolder = provider.getMangaDir(manga, source)
val size = mangaFolder.listFiles()?.size ?: 0
if (size == 0) {
mangaFolder.delete()
cache.removeManga(manga)
} else {
Timber.e("Cache and download folder doesn't match for %s", manga.title)
}
} }
return cleaned return cleaned
} }
@@ -260,7 +285,7 @@ class DownloadManager(/* SY private */ val context: Context) {
* @param manga the manga of the chapters. * @param manga the manga of the chapters.
*/ */
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) { fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
pendingDeleter.addChapters(chapters, manga) pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
} }
/** /**
@@ -289,14 +314,22 @@ class DownloadManager(/* SY private */ val context: Context) {
// Assume there's only 1 version of the chapter name formats present // Assume there's only 1 version of the chapter name formats present
val oldFolder = oldNames.asSequence() val oldFolder = oldNames.asSequence()
.mapNotNull { mangaDir.findFile(it) } .mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
.firstOrNull() .firstOrNull()
if (oldFolder?.renameTo(newName) == true) { if (oldFolder?.renameTo(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "") == true) {
cache.removeChapter(oldChapter, manga) cache.removeChapter(oldChapter, manga)
cache.addChapter(newName, mangaDir, manga) cache.addChapter(newName + if (oldFolder.name?.endsWith(".cbz") == true) ".cbz" else "", mangaDir, manga)
} else { } else {
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString()) Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
} }
} }
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
return if (!preferences.removeBookmarkedChapters()) {
chapters.filterNot { it.bookmark }
} else {
chapters
}
}
} }
@@ -12,8 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.chop 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 uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.regex.Pattern
/** /**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters. * DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@@ -107,7 +107,9 @@ internal class DownloadNotifier(private val context: Context) {
} }
val downloadingProgressText = context.getString( val downloadingProgressText = context.getString(
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size R.string.chapter_downloading_progress,
download.downloadedImages,
download.pages!!.size
) )
if (preferences.hideNotificationContent()) { if (preferences.hideNotificationContent()) {
@@ -89,7 +89,7 @@ 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 getValidChapterDirNames(chapter).asSequence() return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it) } .mapNotNull { mangaDir?.findFile(it) ?: mangaDir?.findFile("$it.cbz") }
.firstOrNull() .firstOrNull()
} }
@@ -104,7 +104,7 @@ class DownloadProvider(private val context: Context) {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { chapter -> return chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter).asSequence() getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir.findFile(it) } .mapNotNull { mangaDir.findFile(it) ?: mangaDir.findFile("$it.cbz") }
.firstOrNull() .firstOrNull()
} }
} }
@@ -127,10 +127,10 @@ class DownloadProvider(private val context: Context) {
( (
chapters.find { chp -> chapters.find { chp ->
getValidChapterDirNames(chp).any { dir -> getValidChapterDirNames(chp).any { dir ->
mangaDir.findFile(dir) != null mangaDir.findFile(dir) ?: mangaDir.findFile("$dir.cbz") != null
} }
} == null } == null
) || it.name?.endsWith("_tmp") == true ) || it.name?.endsWith(Downloader.TMP_DIR_SUFFIX) == true
} }
} }
// SY <-- // SY <--
@@ -11,6 +11,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.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import java.io.File
import kotlinx.coroutines.async import kotlinx.coroutines.async
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@@ -30,7 +30,13 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.BufferedOutputStream
import java.io.File
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/** /**
* This class is the one in charge of downloading chapters. * This class is the one in charge of downloading chapters.
@@ -53,6 +59,8 @@ class Downloader(
private val sourceManager: SourceManager private val sourceManager: SourceManager
) { ) {
private val preferences: PreferencesHelper by injectLazy()
private val chapterCache: ChapterCache by injectLazy() private val chapterCache: ChapterCache by injectLazy()
/** /**
@@ -464,7 +472,39 @@ class Downloader(
// Only rename the directory if it's downloaded. // Only rename the directory if it's downloaded.
if (download.status == Download.DOWNLOADED) { if (download.status == Download.DOWNLOADED) {
tmpDir.renameTo(dirname) if (preferences.saveChaptersAsCBZ().get()) {
val zip = mangaDir.createFile("$dirname.cbz.tmp")
val zipOut = ZipOutputStream(BufferedOutputStream(zip.openOutputStream()))
val compressionLevel = preferences.saveChaptersAsCBZLevel().get()
zipOut.setLevel(compressionLevel)
if (compressionLevel == 0) {
zipOut.setMethod(ZipEntry.STORED)
}
tmpDir.listFiles()?.forEach { img ->
val input = img.openInputStream()
val data = input.readBytes()
val entry = ZipEntry(img.name)
if (compressionLevel == 0) {
val crc = CRC32()
val size = img.length()
crc.update(data)
entry.crc = crc.value
entry.compressedSize = size
entry.size = size
}
zipOut.putNextEntry(entry)
zipOut.write(data)
input.close()
}
zipOut.close()
zip.renameTo("$dirname.cbz")
tmpDir.delete()
} else {
tmpDir.renameTo(dirname)
}
cache.addChapter(dirname, mangaDir, download.manga) cache.addChapter(dirname, mangaDir, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context) DiskUtil.createNoMediaFile(tmpDir, context)
@@ -5,9 +5,9 @@ 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.DownloadStore import eu.kanade.tachiyomi.data.download.DownloadStore
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import java.util.concurrent.CopyOnWriteArrayList
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue( class DownloadQueue(
private val store: DownloadStore, private val store: DownloadStore,
@@ -5,12 +5,12 @@ import android.util.Log
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import timber.log.Timber
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> { open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
@@ -14,10 +14,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
import java.io.InputStream
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.InputStream
/** /**
* A class for loading a cover associated with a [Manga] that can be present in our own cache. * A class for loading a cover associated with a [Manga] that can be present in our own cache.
@@ -14,9 +14,9 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import java.io.InputStream
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.InputStream
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
@@ -29,7 +29,8 @@ class CustomMangaManager(val context: Context) {
val json = try { val json = try {
Gson().fromJson( Gson().fromJson(
Scanner(editJson).useDelimiter("\\Z").next(), JsonObject::class.java Scanner(editJson).useDelimiter("\\Z").next(),
JsonObject::class.java
) )
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -83,7 +84,12 @@ class CustomMangaManager(val context: Context) {
fun Manga.toJson(): MangaJson { fun Manga.toJson(): MangaJson {
return MangaJson( return MangaJson(
id!!, title, author, artist, description, genre?.split(", ")?.toTypedArray() id!!,
title,
author,
artist,
description,
genre?.split(", ")?.toTypedArray()
) )
} }
@@ -9,9 +9,9 @@ import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
@@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.build() .build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
interval.toLong(), TimeUnit.HOURS, interval.toLong(),
10, TimeUnit.MINUTES TimeUnit.HOURS,
10,
TimeUnit.MINUTES
) )
.addTag(TAG) .addTag(TAG)
.setConstraints(constraints) .setConstraints(constraints)
@@ -17,14 +17,15 @@ import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
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 uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.DecimalFormatSymbols import java.text.DecimalFormatSymbols
import uy.kohesive.injekt.injectLazy
class LibraryUpdateNotifier(private val context: Context) { class LibraryUpdateNotifier(private val context: Context) {
@@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
* @param current the current progress. * @param current the current progress.
* @param total the total progress. * @param total the total progress.
*/ */
fun showProgressNotification(manga: Manga, current: Int, total: Int) { fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) {
val title = if (preferences.hideNotificationContent()) { val title = if (preferences.hideNotificationContent()) {
context.getString(R.string.notification_check_updates) context.getString(R.string.notification_check_updates)
} else { } else {
@@ -198,18 +199,23 @@ class LibraryUpdateNotifier(private val context: Context) {
// Mark chapters as read action // Mark chapters as read action
addAction( addAction(
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read), R.drawable.ic_glasses_black_24dp,
context.getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast( NotificationReceiver.markAsReadPendingBroadcast(
context, context,
manga, chapters, Notifications.ID_NEW_CHAPTERS manga,
chapters,
Notifications.ID_NEW_CHAPTERS
) )
) )
// View chapters action // View chapters action
addAction( addAction(
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters), R.drawable.ic_book_24dp,
context.getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga, Notifications.ID_NEW_CHAPTERS manga,
Notifications.ID_NEW_CHAPTERS
) )
) )
} }
@@ -6,6 +6,8 @@ import android.content.Intent
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 com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
@@ -17,10 +19,15 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
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.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
@@ -28,14 +35,25 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import java.io.File import exh.MERGED_SOURCE_ID
import java.util.concurrent.atomic.AtomicInteger import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.asObservable
import exh.util.await
import exh.util.awaitSingle
import exh.util.nullIfBlank
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Date
import java.util.concurrent.atomic.AtomicInteger
/** /**
* This class will take care of updating the chapters of the manga from the library. It can be * This class will take care of updating the chapters of the manga from the library. It can be
@@ -72,7 +90,10 @@ class LibraryUpdateService(
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
COVERS, // Manga covers COVERS, // Manga covers
TRACKING // Tracking metadata TRACKING, // Tracking metadata
// SY -->
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
// SY <--
} }
companion object { companion object {
@@ -87,6 +108,14 @@ class LibraryUpdateService(
*/ */
const val KEY_TARGET = "target" const val KEY_TARGET = "target"
// SY -->
/**
* Key for group to update.
*/
const val KEY_GROUP = "group"
const val KEY_GROUP_EXTRA = "group_extra"
// SY <--
/** /**
* Returns the status of the service. * Returns the status of the service.
* *
@@ -106,11 +135,15 @@ class LibraryUpdateService(
* @param target defines what should be updated. * @param target defines what should be updated.
* @return true if service newly started, false otherwise * @return true if service newly started, false otherwise
*/ */
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean { fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): Boolean {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply { val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target) putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) } category?.let { putExtra(KEY_CATEGORY, it.id) }
// SY -->
putExtra(KEY_GROUP, group)
groupExtra?.let { putExtra(KEY_GROUP_EXTRA, it) }
// SY <--
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent) context.startService(intent)
@@ -194,6 +227,9 @@ class LibraryUpdateService(
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList) Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings(mangaList)
// SY -->
Target.SYNC_FOLLOWS -> syncFollows()
// SY <--
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@@ -221,10 +257,15 @@ class LibraryUpdateService(
*/ */
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
// SY -->
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
// SY <--
var listToUpdate = if (categoryId != -1) { var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
} else { // SY -->
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) { if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking() db.getLibraryMangas().executeAsBlocking()
@@ -233,6 +274,43 @@ class LibraryUpdateService(
} else { } else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
} }
} else {
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
when (group) {
LibraryGroup.BY_TRACK_STATUS -> {
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter {
val loggedServices = trackManager.services.filter { it.isLogged }
val status: String = {
val tracks = db.getTracks(it).executeAsBlocking()
val track = tracks.find { track ->
loggedServices.any { it.id == track?.sync_id }
}
val service = loggedServices.find { it.id == track?.sync_id }
if (track != null && service != null) {
service.getStatus(track.status)
} else {
"not tracked"
}
}()
trackManager.mapTrackingOrder(status, applicationContext) == trackingExtra
}
}
LibraryGroup.BY_SOURCE -> {
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank()
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
}
LibraryGroup.BY_STATUS -> {
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1
libraryManga.filter {
it.status == statusExtra
}
}
LibraryGroup.UNGROUPED -> libraryManga
else -> libraryManga
}
// SY <--
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
@@ -275,7 +353,12 @@ class LibraryUpdateService(
updateManga(manga) updateManga(manga)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(Pair(manga, it.message)) val errorMessage = if (it is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
it.message
}
failedUpdates.add(Pair(manga, errorMessage))
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
@@ -328,7 +411,12 @@ class LibraryUpdateService(
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) { private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
// We don't want to start downloading while the library is updating, because websites // We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user. // may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, chapters, false) // SY -->
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) {
db.getMergedMangaReferences(manga.id!!).executeAsBlocking().filterNot { it.downloadChapters }.mapNotNull { it.mangaId }
} else emptyList()
// SY <--
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filter { it.manga_id !in chapterFilter } /* SY <-- */, false)
} }
/** /**
@@ -338,7 +426,7 @@ class LibraryUpdateService(
* @return a pair of the inserted and removed chapters. * @return a pair of the inserted and removed chapters.
*/ */
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) ?: return Observable.empty() val source = sourceManager.getOrStub(manga.source)
// Update manga details metadata in the background // Update manga details metadata in the background
if (preferences.autoUpdateMetadata()) { if (preferences.autoUpdateMetadata()) {
@@ -360,8 +448,38 @@ class LibraryUpdateService(
.subscribe() .subscribe()
} }
return source.fetchChapterList(manga) // SY -->
.map { syncChaptersWithSource(db, it, manga, source) } if (source.getMainSource() is MangaDex && trackManager.mdList.isLogged) {
try {
val tracks = db.getTracks(manga).executeAsBlocking()
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
var track = trackManager.mdList.createInitialTracker(manga)
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
db.insertTrack(track).executeAsBlocking()
}
} catch (e: Exception) {
XLog.e(e)
}
}
// SY <--
return (
/* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
else /* SY <-- */ source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
// SY -->
)
.doOnNext {
if (source.getMainSource() is MangaDex) {
val tracks = db.getTracks(manga).executeAsBlocking()
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
var track = trackManager.mdList.createInitialTracker(manga)
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
db.insertTrack(track).executeAsBlocking()
}
}
}
// SY <--
} }
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> { private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
@@ -427,6 +545,48 @@ class LibraryUpdateService(
} }
} }
// SY -->
// filter all follows from Mangadex and only add reading or rereading manga to library
private fun syncFollows(): Observable<LibraryManga> {
val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager)!!
return mangaDex.fetchAllFollows(true)
.asObservable()
.map { listManga ->
listManga.filter { (_, metadata) ->
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
}
}
.doOnNext { listManga ->
listManga.forEach { (networkManga, metadata) ->
notifier.showProgressNotification(networkManga, count.andIncrement, listManga.size)
var dbManga = db.getManga(networkManga.url, mangaDex.id)
.executeAsBlocking()
if (dbManga == null) {
dbManga = Manga.create(
networkManga.url,
networkManga.title,
mangaDex.id
)
dbManga.date_added = Date().time
}
dbManga.copyFrom(networkManga)
dbManga.favorite = true
val id = db.insertManga(dbManga).executeAsBlocking().insertedId()
if (id != null) {
metadata.mangaId = id
db.insertFlatMetadata(metadata.flatten()).await()
}
}
}
.doOnCompleted {
notifier.cancelProgressNotification()
}
.map { LibraryManga() }
}
// SY <--
/** /**
* Writes basic file of update errors to cache dir. * Writes basic file of update errors to cache dir.
*/ */
@@ -437,7 +597,8 @@ class LibraryUpdateService(
destFile.bufferedWriter().use { out -> destFile.bufferedWriter().use { out ->
errors.forEach { (manga, error) -> errors.forEach { (manga, error) ->
out.write("${manga.title}: $error\n") val source = sourceManager.getOrStub(manga.source)
out.write("${manga.title} ($source): $error\n")
} }
} }
return destFile return destFile
@@ -7,7 +7,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -26,10 +25,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/** /**
* Global [BroadcastReceiver] that runs on UI thread * Global [BroadcastReceiver] that runs on UI thread
@@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() {
// Launch share activity and dismiss notification // Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> ACTION_SHARE_IMAGE ->
shareImage( shareImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION), context,
intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
// Delete image from path and dismiss notification // Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> ACTION_DELETE_IMAGE ->
deleteImage( deleteImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION), context,
intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
// Share backup file // Share backup file
ACTION_SHARE_BACKUP -> ACTION_SHARE_BACKUP ->
shareBackup( shareBackup(
context, intent.getParcelableExtra(EXTRA_URI), context,
intent.getParcelableExtra(EXTRA_URI),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
ACTION_CANCEL_RESTORE -> cancelRestore( ACTION_CANCEL_RESTORE -> cancelRestore(
@@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() {
// Open reader activity // Open reader activity
ACTION_OPEN_CHAPTER -> { ACTION_OPEN_CHAPTER -> {
openChapter( openChapter(
context, intent.getLongExtra(EXTRA_MANGA_ID, -1), context,
intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1) intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
) )
} }
@@ -82,53 +82,62 @@ object Notifications {
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
), ),
NotificationChannel( NotificationChannel(
CHANNEL_LIBRARY, context.getString(R.string.channel_library), CHANNEL_LIBRARY,
context.getString(R.string.channel_library),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress), CHANNEL_DOWNLOADER_PROGRESS,
context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_DOWNLOADER group = GROUP_DOWNLOADER
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete), CHANNEL_DOWNLOADER_COMPLETE,
context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_DOWNLOADER group = GROUP_DOWNLOADER
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_DOWNLOADER_ERROR, context.getString(R.string.channel_errors), CHANNEL_DOWNLOADER_ERROR,
context.getString(R.string.channel_errors),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_DOWNLOADER group = GROUP_DOWNLOADER
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters), CHANNEL_NEW_CHAPTERS,
context.getString(R.string.channel_new_chapters),
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
), ),
NotificationChannel( NotificationChannel(
CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates), CHANNEL_UPDATES_TO_EXTS,
context.getString(R.string.channel_ext_updates),
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
), ),
NotificationChannel( NotificationChannel(
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress), CHANNEL_BACKUP_RESTORE_PROGRESS,
context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_BACKUP_RESTORE group = GROUP_BACKUP_RESTORE
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete), CHANNEL_BACKUP_RESTORE_COMPLETE,
context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
group = GROUP_BACKUP_RESTORE group = GROUP_BACKUP_RESTORE
@@ -115,6 +115,8 @@ object PreferenceKeys {
const val filterCompleted = "pref_filter_completed_key" const val filterCompleted = "pref_filter_completed_key"
const val filterStarted = "pref_filter_started_key"
const val filterTracked = "pref_filter_tracked_key" const val filterTracked = "pref_filter_tracked_key"
const val filterLewd = "pref_filter_lewd_key" const val filterLewd = "pref_filter_lewd_key"
@@ -280,4 +282,40 @@ object PreferenceKeys {
const val startReadingButton = "start_reading_button" const val startReadingButton = "start_reading_button"
const val groupLibraryBy = "group_library_by" const val groupLibraryBy = "group_library_by"
const val continuousVerticalTappingByPage = "continuous_vertical_tapping_by_page"
const val groupLibraryUpdateType = "group_library_update_type"
const val useNewSourceNavigation = "use_new_source_navigation"
const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers"
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
const val preferredMangaDexId = "preferred_mangaDex_id"
const val dataSaver = "data_saver"
const val ignoreJpeg = "ignore_jpeg"
const val ignoreGif = "ignore_gif"
const val dataSaverImageQuality = "data_saver_image_quality"
const val dataSaverImageFormatJpeg = "data_saver_image_format_jpeg"
const val dataSaverServer = "data_saver_server"
const val dataSaverColorBW = "data_saver_color_bw"
const val saveChaptersAsCBZ = "save_chapter_as_cbz"
const val saveChaptersAsCBZLevel = "save_chapter_as_cbz_level"
const val allowLocalSourceHiddenFolders = "allow_local_source_hidden_folders"
const val biometricTimeRanges = "biometric_time_ranges"
const val sortTagsForLibrary = "sort_tags_for_library"
} }
@@ -23,11 +23,15 @@ object PreferenceValues {
default, default,
blue, blue,
amoled, amoled,
red,
} }
enum class DisplayMode { enum class DisplayMode {
COMPACT_GRID, COMPACT_GRID,
COMFORTABLE_GRID, COMFORTABLE_GRID,
// SY -->
NO_TITLE_GRID,
// SY <--
LIST, LIST,
} }
@@ -43,4 +47,12 @@ object PreferenceValues {
PARTIAL, PARTIAL,
BLOCKED BLOCKED
} }
// SY -->
enum class GroupLibraryMode {
GLOBAL,
ALL_BUT_UNGROUPED,
ALL
}
// SY <--
} }
@@ -7,19 +7,19 @@ import androidx.preference.PreferenceManager
import com.tfcporciuncula.flow.FlowSharedPreferences import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance 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 kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import kotlinx.coroutines.flow.Flow import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import kotlinx.coroutines.flow.onEach
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> { fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
@@ -215,6 +215,8 @@ class PreferencesHelper(val context: Context) {
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0) fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, 0)
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 0)
fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0) fun filterTracked() = flowPrefs.getInt(Keys.filterTracked, 0)
fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0) fun filterLewd() = flowPrefs.getInt(Keys.filterLewd, 0)
@@ -354,7 +356,7 @@ 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_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 4) fun eh_preload_size() = flowPrefs.getInt(Keys.eh_preload_size, 10)
fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true) fun eh_useAutoWebtoon() = flowPrefs.getBoolean(Keys.eh_use_auto_webtoon, true)
@@ -385,4 +387,40 @@ class PreferencesHelper(val context: Context) {
fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true) fun startReadingButton() = flowPrefs.getBoolean(Keys.startReadingButton, true)
fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0) fun groupLibraryBy() = flowPrefs.getInt(Keys.groupLibraryBy, 0)
fun continuousVerticalTappingByPage() = flowPrefs.getBoolean(Keys.continuousVerticalTappingByPage, false)
fun groupLibraryUpdateType() = flowPrefs.getEnum(Keys.groupLibraryUpdateType, Values.GroupLibraryMode.GLOBAL)
fun useNewSourceNavigation() = flowPrefs.getBoolean(Keys.useNewSourceNavigation, true)
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false)
fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false)
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
fun ignoreGif() = flowPrefs.getBoolean(Keys.ignoreGif, true)
fun dataSaverImageQuality() = flowPrefs.getInt(Keys.dataSaverImageQuality, 80)
fun dataSaverImageFormatJpeg() = flowPrefs.getBoolean(Keys.dataSaverImageFormatJpeg, false)
fun dataSaverServer() = flowPrefs.getString(Keys.dataSaverServer, "")
fun dataSaverColorBW() = flowPrefs.getBoolean(Keys.dataSaverColorBW, false)
fun saveChaptersAsCBZ() = flowPrefs.getBoolean(Keys.saveChaptersAsCBZ, false)
fun saveChaptersAsCBZLevel() = flowPrefs.getInt(Keys.saveChaptersAsCBZLevel, 0)
fun allowLocalSourceHiddenFolders() = flowPrefs.getBoolean(Keys.allowLocalSourceHiddenFolders, false)
fun biometricTimeRanges() = flowPrefs.getStringSet(Keys.biometricTimeRanges, mutableSetOf())
fun sortTagsForLibrary() = flowPrefs.getStringSet(Keys.sortTagsForLibrary, mutableSetOf())
} }
@@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
@@ -15,8 +16,24 @@ class TrackManager(context: Context) {
const val KITSU = 3 const val KITSU = 3
const val SHIKIMORI = 4 const val SHIKIMORI = 4
const val BANGUMI = 5 const val BANGUMI = 5
// SY --> Mangadex from Neko
const val MDLIST = 6
// SY <--
// SY -->
const val READING = 1
const val REREADING = 2
const val PLANTOREAD = 3
const val PAUSED = 4
const val COMPLETED = 5
const val DROPPED = 6
const val OTHER = 7
// SY <--
} }
val mdList = MdList(context, MDLIST)
val myAnimeList = MyAnimeList(context, MYANIMELIST) val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST) val aniList = Anilist(context, ANILIST)
@@ -27,9 +44,25 @@ class TrackManager(context: Context) {
val bangumi = Bangumi(context, BANGUMI) val bangumi = Bangumi(context, BANGUMI)
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi) val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
fun getService(id: Int) = services.find { it.id == id } fun getService(id: Int) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged } fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) }
// SY -->
fun mapTrackingOrder(status: String, context: Context): Int {
with(context) {
return when (status) {
getString(eu.kanade.tachiyomi.R.string.reading), getString(eu.kanade.tachiyomi.R.string.currently_reading) -> READING
getString(eu.kanade.tachiyomi.R.string.repeating) -> REREADING
getString(eu.kanade.tachiyomi.R.string.plan_to_read), getString(eu.kanade.tachiyomi.R.string.want_to_read) -> PLANTOREAD
getString(eu.kanade.tachiyomi.R.string.on_hold), getString(eu.kanade.tachiyomi.R.string.paused) -> PAUSED
getString(eu.kanade.tachiyomi.R.string.completed) -> COMPLETED
getString(eu.kanade.tachiyomi.R.string.dropped) -> DROPPED
else -> OTHER
}
}
}
// SY <--
} }
@@ -13,12 +13,12 @@ import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import java.util.Calendar
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable import rx.Observable
import java.util.Calendar
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@@ -271,9 +271,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
return ALManga( return ALManga(
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, struct["id"].asInt,
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(), struct["title"]["romaji"].asString,
date, struct["chapters"].nullInt ?: 0 struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(),
struct["type"].asString,
struct["status"].nullString.orEmpty(),
date,
struct["chapters"].nullInt ?: 0
) )
} }
@@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
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.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import uy.kohesive.injekt.injectLazy
data class ALManga( data class ALManga(
val media_id: Int, val media_id: Int,
@@ -12,13 +12,13 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import java.net.URLEncoder
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) { class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
@@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import java.text.DecimalFormat
import rx.Completable import rx.Completable
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
class Kitsu(private val context: Context, id: Int) : TrackService(id) { class Kitsu(private val context: Context, id: Int) : TrackService(id) {
@@ -0,0 +1,134 @@
package eu.kanade.tachiyomi.data.track.mdlist
import android.content.Context
import android.graphics.Color
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.asObservable
import exh.util.floor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class MdList(private val context: Context, id: Int) : TrackService(id) {
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
private val db: DatabaseHelper by injectLazy()
override val name = "MDList"
override fun getLogo(): Int {
return R.drawable.ic_tracker_mangadex_logo
}
override fun getLogoColor(): Int {
return Color.rgb(43, 48, 53)
}
override fun getStatusList(): List<Int> {
return FollowStatus.values().map { it.int }
}
override fun getStatus(status: Int): String =
context.resources.getStringArray(R.array.md_follows_options).asList()[status]
override fun getScoreList() = IntRange(0, 10).map(Int::toString)
override fun displayScore(track: Track) = track.score.toInt().toString()
override fun add(track: Track): Observable<Track> {
return update(track)
}
override fun update(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return Observable.defer {
db.getManga(track.tracking_url.substringAfter(".org"), mdex.id)
.asRxObservable()
.map { manga ->
val mangaMetadata = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise(MangaDexSearchMetadata::class) ?: throw Exception("Invalid manga metadata")
val followStatus = FollowStatus.fromInt(track.status)!!
// allow follow status to update
if (mangaMetadata.follow_status != followStatus.int) {
runBlocking { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus).collect() }
mangaMetadata.follow_status = followStatus.int
db.insertFlatMetadata(mangaMetadata.flatten()).await()
}
if (track.score.toInt() > 0) {
runBlocking { mdex.updateRating(track).collect() }
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
}
runBlocking { mdex.updateReadingProgress(track).collect() }
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
}
track
}
}
}
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
override fun bind(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
.doOnNext { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = if (remoteTrack.total_chapters == 0) {
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
} else {
remoteTrack.total_chapters
}
update(track)
}
}
override fun refresh(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = if (remoteTrack.total_chapters == 0) {
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
} else {
remoteTrack.total_chapters
}
track
}
}
fun createInitialTracker(manga: Manga): Track {
val track = Track.create(TrackManager.MDLIST)
track.manga_id = manga.id!!
track.status = FollowStatus.UNFOLLOWED.int
track.tracking_url = MdUtil.baseUrl + manga.url
track.title = manga.title
return track
}
override fun search(query: String): Observable<List<TrackSearch>> = throw Exception("not used")
override fun login(username: String, password: String): Completable = throw Exception("not used")
}
@@ -11,13 +11,6 @@ import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.lang.toCalendar import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText import eu.kanade.tachiyomi.util.selectText
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.zip.GZIPInputStream
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -30,6 +23,13 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import rx.Observable import rx.Observable
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.zip.GZIPInputStream
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
@@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
abstract class UpdateChecker {
companion object {
fun getUpdateChecker(): UpdateChecker {
// SY -->
return GithubUpdateChecker()
// SY <--
}
}
/**
* Returns observable containing release information
*/
abstract suspend fun checkForUpdate(): UpdateResult
}
@@ -13,9 +13,10 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) : class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) { Worker(context, workerParams) {
@@ -23,7 +24,7 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
override fun doWork(): Result { override fun doWork(): Result {
return runBlocking { return runBlocking {
try { try {
val result = UpdateChecker.getUpdateChecker().checkForUpdate() val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) { if (result is UpdateResult.NewUpdate<*>) {
val url = result.release.downloadLink val url = result.release.downloadLink
@@ -65,8 +66,10 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
.build() .build()
val request = PeriodicWorkRequestBuilder<UpdaterJob>( val request = PeriodicWorkRequestBuilder<UpdaterJob>(
3, TimeUnit.DAYS, 3,
3, TimeUnit.HOURS TimeUnit.DAYS,
3,
TimeUnit.HOURS
) )
.addTag(TAG) .addTag(TAG)
.setConstraints(constraints) .setConstraints(constraints)
@@ -20,9 +20,9 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
class UpdaterService : Service() { class UpdaterService : Service() {
@@ -1,13 +0,0 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.data.updater.Release
class DevRepoRelease(override val info: String) : Release {
override val downloadLink: String
get() = LATEST_URL
companion object {
const val LATEST_URL = "https://tachiyomi.kanade.eu/latest"
}
}
@@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class DevRepoUpdateChecker : UpdateChecker() {
private val client: OkHttpClient by lazy {
Injekt.get<NetworkHelper>().client.newBuilder()
.followRedirects(false)
.build()
}
private val versionRegex: Regex by lazy {
Regex("tachiyomi-r(\\d+).apk")
}
override suspend fun checkForUpdate(): UpdateResult {
val response = withContext(Dispatchers.IO) {
client.newCall(GET(DevRepoRelease.LATEST_URL)).await()
}
// Get latest repo version number from header in format "Location: tachiyomi-r1512.apk"
val latestVersionNumber: String = versionRegex.find(response.header("Location")!!)!!.groupValues[1]
return if (latestVersionNumber.toInt() > BuildConfig.COMMIT_COUNT.toInt()) {
DevRepoUpdateResult.NewUpdate(DevRepoRelease("v$latestVersionNumber"))
} else {
DevRepoUpdateResult.NoNewUpdate()
}
}
}
@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.data.updater.devrepo
import eu.kanade.tachiyomi.data.updater.UpdateResult
sealed class DevRepoUpdateResult : UpdateResult() {
class NewUpdate(release: DevRepoRelease) : UpdateResult.NewUpdate<DevRepoRelease>(release)
class NoNewUpdate : UpdateResult.NoNewUpdate()
}
@@ -28,5 +28,5 @@ class GithubRelease(
* Assets class containing download url. * Assets class containing download url.
* @param downloadLink download url. * @param downloadLink download url.
*/ */
inner class Assets(@SerializedName("browser_download_url") val downloadLink: String) class Assets(@SerializedName("browser_download_url") val downloadLink: String)
} }
@@ -4,11 +4,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
/** /**
* Used to connect with the GitHub API. * Used to connect with the GitHub API to get the latest release version from a repo.
*/ */
interface GithubService { interface GithubService {
@@ -24,11 +25,6 @@ interface GithubService {
} }
} }
// SY --> @GET("/repos/{repo}/releases/latest")
@GET("/repos/jobobby04/tachiyomiSY/releases/latest") suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
suspend fun getLatestVersion(): GithubRelease
@GET("/repos/jobobby04/TachiyomiSYPreview/releases/latest")
suspend fun getLatestDebugVersion(): GithubRelease
// SY <--
} }
@@ -1,28 +1,49 @@
package eu.kanade.tachiyomi.data.updater.github package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.UpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateResult import eu.kanade.tachiyomi.data.updater.UpdateResult
import exh.syDebugVersion import exh.syDebugVersion
// SY -->
class GithubUpdateChecker(val debug: Boolean = false) : UpdateChecker() { class GithubUpdateChecker {
private val service: GithubService = GithubService.create() private val service: GithubService = GithubService.create()
override suspend fun checkForUpdate(): UpdateResult { private val repo: String by lazy {
val release = if (syDebugVersion != "0") { // Sy -->
service.getLatestDebugVersion() if (syDebugVersion != "0") {
"jobobby04/TachiyomiSYPreview"
} else { } else {
service.getLatestVersion() "jobobby04/tachiyomiSY"
} }
// SY <--
}
suspend fun checkForUpdate(): UpdateResult {
val release = service.getLatestVersion(repo)
val newVersion = release.version
// Check if latest version is different from current version // Check if latest version is different from current version
// SY -->
val newVersion = release.version
return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) { return if ((newVersion != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && newVersion != syDebugVersion)) {
// SY <--
GithubUpdateResult.NewUpdate(release) GithubUpdateResult.NewUpdate(release)
} else { } else {
GithubUpdateResult.NoNewUpdate() GithubUpdateResult.NoNewUpdate()
} }
} }
private fun isNewVersion(versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.DEBUG) {
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} else {
// Release builds: based on releases in "inorichi/tachiyomi" repo
// tagged as something like "v0.1.2"
newVersion != BuildConfig.VERSION_NAME
}
}
} }
// SY <--
@@ -16,10 +16,10 @@ 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.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) : class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) { CoroutineWorker(context, workerParams) {
@@ -73,8 +73,10 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
.build() .build()
val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>( val request = PeriodicWorkRequestBuilder<ExtensionUpdateJob>(
12, TimeUnit.HOURS, 12,
1, TimeUnit.HOURS TimeUnit.HOURS,
1,
TimeUnit.HOURS
) )
.addTag(TAG) .addTag(TAG)
.setConstraints(constraints) .setConstraints(constraints)
@@ -10,10 +10,10 @@ 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 exh.source.BlacklistedSources import exh.source.BlacklistedSources
import java.util.Date
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
@@ -13,11 +13,11 @@ 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
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import java.io.File
import java.util.concurrent.TimeUnit
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
/** /**
* The installer which installs, updates and uninstalls the extensions. * The installer which installs, updates and uninstalls the extensions.
@@ -178,7 +178,13 @@ internal object ExtensionLoader {
} }
val extension = Extension.Installed( val extension = Extension.Installed(
extName, pkgName, versionName, versionCode, lang, isNsfw, sources, extName,
pkgName,
versionName,
versionCode,
lang,
isNsfw,
sources,
isUnofficial = signatureHash != officialSignature isUnofficial = signatureHash != officialSignature
) )
return LoadResult.Success(extension) return LoadResult.Success(extension)
@@ -16,15 +16,15 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor(private val context: Context) : Interceptor { class CloudflareInterceptor(private val context: Context) : Interceptor {
@@ -89,7 +89,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
var isWebViewOutdated = false var isWebViewOutdated = false
val origRequestUrl = request.url.toString() val origRequestUrl = request.url.toString()
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" } val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
headers["X-Requested-With"] = WebViewUtil.REQUESTED_WITH
handler.post { handler.post {
val webview = WebView(context) val webview = WebView(context)
@@ -3,15 +3,15 @@ package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit
import okhttp3.Cache import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) { /* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
@@ -1,9 +1,5 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
@@ -13,6 +9,10 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -54,22 +54,24 @@ fun Call.asObservable(): Observable<Response> {
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
suspend fun Call.await(assertSuccess: Boolean = false): Response { suspend fun Call.await(assertSuccess: Boolean = false): Response {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback { enqueue(
override fun onResponse(call: Call, response: Response) { object : Callback {
if (assertSuccess && !response.isSuccessful) { override fun onResponse(call: Call, response: Response) {
continuation.resumeWithException(Exception("HTTP error ${response.code}")) if (assertSuccess && !response.isSuccessful) {
return continuation.resumeWithException(Exception("HTTP error ${response.code}"))
return
}
continuation.resume(response)
} }
continuation.resume(response) override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
} }
)
override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation { continuation.invokeOnCancellation {
try { try {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import java.io.IOException
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okio.Buffer import okio.Buffer
@@ -8,6 +7,7 @@ import okio.BufferedSource
import okio.ForwardingSource import okio.ForwardingSource
import okio.Source import okio.Source
import okio.buffer import okio.buffer
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() { class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
@@ -1,11 +1,11 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import java.util.concurrent.TimeUnit.MINUTES
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import java.util.concurrent.TimeUnit.MINUTES
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build() private val DEFAULT_HEADERS = Headers.Builder().build()
@@ -4,6 +4,7 @@ import android.content.Context
import com.google.gson.GsonBuilder 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.data.preference.PreferencesHelper
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.MangasPage
@@ -15,6 +16,12 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import junrar.Archive
import junrar.rarfile.FileHeader
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
@@ -22,10 +29,6 @@ import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
import junrar.Archive
import junrar.rarfile.FileHeader
import rx.Observable
import timber.log.Timber
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(private val context: Context) : CatalogueSource {
companion object { companion object {
@@ -74,6 +77,9 @@ class LocalSource(private val context: Context) : CatalogueSource {
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val baseDirs = getBaseDirectories(context) val baseDirs = getBaseDirectories(context)
// SY -->
val allowLocalSourceHiddenFolders = Injekt.get<PreferencesHelper>().allowLocalSourceHiddenFolders().get()
// SY <--
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 var mangaDirs = baseDirs
@@ -81,6 +87,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
.mapNotNull { it.listFiles()?.toList() } .mapNotNull { it.listFiles()?.toList() }
.flatten() .flatten()
.filter { it.isDirectory } .filter { it.isDirectory }
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } .filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name } .distinctBy { it.name }
@@ -11,6 +11,7 @@ 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.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.Hitomi import eu.kanade.tachiyomi.source.online.all.Hitomi
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden import eu.kanade.tachiyomi.source.online.all.PervEden
@@ -31,13 +32,13 @@ 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
import kotlin.reflect.KClass
import kotlinx.coroutines.CoroutineScope 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 rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.reflect.KClass
open class SourceManager(private val context: Context) { open class SourceManager(private val context: Context) {
@@ -191,14 +192,14 @@ open class SourceManager(private val context: Context) {
TSUMINO_SOURCE_ID, TSUMINO_SOURCE_ID,
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class Tsumino::class
)/*, ),
DelegatedSource( DelegatedSource(
"MangaDex", "MangaDex",
fillInSourceId, fillInSourceId,
"eu.kanade.tachiyomi.extension.all.mangadex", "eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class, MangaDex::class,
true true
)*/, ),
DelegatedSource( DelegatedSource(
"HBrowse", "HBrowse",
HBROWSE_SOURCE_ID, HBROWSE_SOURCE_ID,
@@ -2,16 +2,26 @@ package eu.kanade.tachiyomi.source.model
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import exh.util.DataSaver
import rx.subjects.Subject import rx.subjects.Subject
open class Page( open class Page(
val index: Int, val index: Int,
/* SY --> */ /* SY --> */
var /* SY <-- */ url: String = "", var /* SY <-- */ url: String = "",
var imageUrl: String? = null, /* SY --> var <-- SY */
imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
) : ProgressListener { ) : ProgressListener {
// SY -->
var imageUrl = imageUrl
get() {
if (field == null) return null
return DataSaver().compress(field!!)
}
// SY <--
val number: Int val number: Int
get() = index + 1 get() = index + 1
@@ -75,6 +75,11 @@ 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 --> Mangadex specific statuses
const val PUBLICATION_COMPLETE = 61
const val CANCELLED = 62
const val HIATUS = 63
// SY <--
fun create(): SManga { fun create(): SManga {
return SMangaImpl() return SMangaImpl()
@@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source.online
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Controller
interface BrowseSourceFilterHeader {
fun getFilterHeader(controller: Controller): RecyclerView.Adapter<*>
}
@@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.utils.FollowStatus
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.flow.Flow
import rx.Observable
interface FollowsSource {
fun fetchFollows(): Observable<MangasPage>
/**
* Returns a list of all Follows retrieved by Coroutines
*
* @param SManga all smanga found for user
*/
fun fetchAllFollows(forceHd: Boolean = false): Flow<List<Pair<SManga, RaisedSearchMetadata>>>
/**
* updates the follow status for a manga
*/
fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean>
/**
* Get a MdList Track of the manga
*/
fun fetchTrackingInfo(url: String): Flow<Track>
}
@@ -13,11 +13,9 @@ import eu.kanade.tachiyomi.source.model.MangasPage
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 exh.log.maybeInjectEHLogger
import exh.patch.injectPatches import exh.patch.injectPatches
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -25,6 +23,9 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
/** /**
* A simple implementation for sources from a website. * A simple implementation for sources from a website.
@@ -36,22 +37,24 @@ abstract class HttpSource : CatalogueSource {
*/ */
// SY --> // SY -->
protected val network: NetworkHelper by lazy { protected val network: NetworkHelper by lazy {
val original = Injekt.get<NetworkHelper>() val network = Injekt.get<NetworkHelper>()
object : NetworkHelper(Injekt.get<Application>()) { object : NetworkHelper(Injekt.get<Application>()) {
override val client: OkHttpClient override val client: OkHttpClient
get() = delegate?.networkHttpClient ?: original.client get() = delegate?.networkHttpClient ?: network.client
.newBuilder() .newBuilder()
.injectPatches { id } .injectPatches { id }
.maybeInjectEHLogger()
.build() .build()
override val cloudflareClient: OkHttpClient override val cloudflareClient: OkHttpClient
get() = delegate?.networkCloudflareClient ?: original.cloudflareClient get() = delegate?.networkCloudflareClient ?: network.cloudflareClient
.newBuilder() .newBuilder()
.injectPatches { id } .injectPatches { id }
.maybeInjectEHLogger()
.build() .build()
override val cookieManager: AndroidCookieJar override val cookieManager: AndroidCookieJar
get() = original.cookieManager get() = network.cookieManager
} }
} }
// SY <-- // SY <--
@@ -88,7 +91,7 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Headers used for requests. * Headers used for requests.
*/ */
val headers: Headers by lazy { headersBuilder().build() } /* SY --> */ open /* SY <-- */ val headers: Headers by lazy { headersBuilder().build() }
/** /**
* Default network client for doing requests. * Default network client for doing requests.
@@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource {
val needsLogin: Boolean
fun isLogged(): Boolean
fun getLoginDialog(source: Source, activity: Activity): DialogController
suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
suspend fun logout(): Boolean
}
@@ -13,16 +13,16 @@ 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 exh.source.EnhancedHttpSource
import kotlin.reflect.KClass
import rx.Completable import rx.Completable
import rx.Single import rx.Single
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.reflect.KClass
/** /**
* LEWD! * LEWD!
*/ */
interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource { interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
val db: DatabaseHelper get() = Injekt.get() val db: DatabaseHelper get() = Injekt.get()
/** /**
@@ -55,8 +55,7 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
Single.fromCallable { Single.fromCallable {
db.getFlatMetadataForManga(mangaId).executeAsBlocking() db.getFlatMetadataForManga(mangaId).executeAsBlocking()
}.map { }.map {
if (it != null) it.raise(metaClass) it?.raise(metaClass) ?: newMetaInstance()
else newMetaInstance()
} }
} else { } else {
Single.just(newMetaInstance()) Single.just(newMetaInstance())
@@ -112,17 +111,14 @@ interface LewdSource<M : RaisedSearchMetadata, I> : CatalogueSource {
val SChapter.mangaId get() = (this as? Chapter)?.manga_id val SChapter.mangaId get() = (this as? Chapter)?.manga_id
companion object { companion object {
fun Source.isLewdSource() = (this is LewdSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>)) fun Source.isMetadataSource() = (this is MetadataSource<*, *> || (this is EnhancedHttpSource && this.enhancedSource is MetadataSource<*, *>))
fun Source.getLewdSource(): LewdSource<*, *>? { fun Source.getMetadataSource(): MetadataSource<*, *>? {
return if (!this.isLewdSource()) { return when {
null !this.isMetadataSource() -> null
} else if (this is LewdSource<*, *>) { this is MetadataSource<*, *> -> this
this this is EnhancedHttpSource && this.enhancedSource is MetadataSource<*, *> -> this.enhancedSource
} else if (this is EnhancedHttpSource && this.enhancedSource is LewdSource<*, *>) { else -> null
this.enhancedSource
} else {
null
} }
} }
} }
@@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.source.online
import kotlinx.coroutines.flow.Flow
interface RandomMangaSource {
fun fetchRandomMangaUrl(): Flow<String>
}
@@ -0,0 +1,399 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import exh.util.asObservable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import kotlin.jvm.Throws
/**
* A simple implementation for sources from a website, but for Coroutines.
*/
abstract class SuspendHttpSource : HttpSource() {
/**
* 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.
*/
@ExperimentalCoroutinesApi
final override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return fetchPopularMangaFlow(page).asObservable()
}
open fun fetchPopularMangaFlow(page: Int): Flow<MangasPage> {
return flow {
val response = client.newCall(popularMangaRequestSuspended(page)).await()
emit(
popularMangaParseSuspended(response)
)
}
}
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
final override fun popularMangaRequest(page: Int): Request {
return runBlocking { popularMangaRequestSuspended(page) }
}
protected abstract suspend fun popularMangaRequestSuspended(page: Int): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
final override fun popularMangaParse(response: Response): MangasPage {
return runBlocking { popularMangaParseSuspended(response) }
}
protected abstract suspend fun popularMangaParseSuspended(response: Response): MangasPage
/**
* 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.
*/
@ExperimentalCoroutinesApi
final override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return fetchSearchMangaSuspended(page, query, filters).asObservable()
}
open fun fetchSearchMangaSuspended(page: Int, query: String, filters: FilterList): Flow<MangasPage> {
return flow {
val response = client.newCall(searchMangaRequestSuspended(page, query, filters)).await()
emit(
searchMangaParseSuspended(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.
*/
final override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return runBlocking { searchMangaRequestSuspended(page, query, filters) }
}
protected abstract suspend fun searchMangaRequestSuspended(page: Int, query: String, filters: FilterList): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
final override fun searchMangaParse(response: Response): MangasPage {
return runBlocking { searchMangaParseSuspended(response) }
}
protected abstract suspend fun searchMangaParseSuspended(response: Response): MangasPage
/**
* Returns an observable containing a page with a list of latest manga updates.
*
* @param page the page number to retrieve.
*/
@ExperimentalCoroutinesApi
final override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return fetchLatestUpdatesFlow(page).asObservable()
}
open fun fetchLatestUpdatesFlow(page: Int): Flow<MangasPage> {
return flow {
val response = client.newCall(latestUpdatesRequestSuspended(page)).await()
emit(
latestUpdatesParseSuspended(response)
)
}
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
final override fun latestUpdatesRequest(page: Int): Request {
return runBlocking { latestUpdatesRequestSuspended(page) }
}
protected abstract suspend fun latestUpdatesRequestSuspended(page: Int): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
final override fun latestUpdatesParse(response: Response): MangasPage {
return runBlocking { latestUpdatesParseSuspended(response) }
}
protected abstract suspend fun latestUpdatesParseSuspended(response: Response): MangasPage
/**
* 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.
*/
@ExperimentalCoroutinesApi
final override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return fetchMangaDetailsFlow(manga).asObservable()
}
open fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
return flow {
val response = client.newCall(mangaDetailsRequestSuspended(manga)).await()
emit(
mangaDetailsParseSuspended(response).apply { initialized = true }
)
}
}
/**
* Returns the request for the details of a manga. Override only if it's needed to change the
* url, send different headers or request method like POST.
*
* @param manga the manga to be updated.
*/
final override fun mangaDetailsRequest(manga: SManga): Request {
return runBlocking { mangaDetailsRequestSuspended(manga) }
}
open suspend fun mangaDetailsRequestSuspended(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
final override fun mangaDetailsParse(response: Response): SManga {
return runBlocking { mangaDetailsParseSuspended(response) }
}
protected abstract suspend fun mangaDetailsParseSuspended(response: Response): SManga
/**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method. If a manga is licensed an empty chapter list observable is returned
*
* @param manga the manga to look for chapters.
*/
@ExperimentalCoroutinesApi
final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return try {
fetchChapterListFlow(manga).asObservable()
} catch (e: LicencedException) {
Observable.error(Exception("Licensed - No chapters to show"))
}
}
@Throws(LicencedException::class)
open fun fetchChapterListFlow(manga: SManga): Flow<List<SChapter>> {
return flow {
if (manga.status != SManga.LICENSED) {
val response = client.newCall(chapterListRequestSuspended(manga)).await()
emit(
chapterListParseSuspended(response)
)
} else {
throw LicencedException("Licensed - No chapters to show")
}
}
}
/**
* Returns the request for updating the chapter list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param manga the manga to look for chapters.
*/
final override fun chapterListRequest(manga: SManga): Request {
return runBlocking { chapterListRequestSuspended(manga) }
}
protected open suspend fun chapterListRequestSuspended(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
final override fun chapterListParse(response: Response): List<SChapter> {
return runBlocking { chapterListParseSuspended(response) }
}
protected abstract suspend fun chapterListParseSuspended(response: Response): List<SChapter>
/**
* Returns an observable with the page list for a chapter.
*
* @param chapter the chapter whose page list has to be fetched.
*/
@ExperimentalCoroutinesApi
final override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return fetchPageListFlow(chapter).asObservable()
}
open fun fetchPageListFlow(chapter: SChapter): Flow<List<Page>> {
return flow {
val response = client.newCall(pageListRequestSuspended(chapter)).await()
emit(
pageListParseSuspended(response)
)
}
}
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param chapter the chapter whose page list has to be fetched.
*/
final override fun pageListRequest(chapter: SChapter): Request {
return runBlocking { pageListRequestSuspended(chapter) }
}
protected open suspend fun pageListRequestSuspended(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers)
}
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
final override fun pageListParse(response: Response): List<Page> {
return runBlocking { pageListParseSuspended(response) }
}
protected abstract suspend fun pageListParseSuspended(response: Response): List<Page>
/**
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @param page the page whose source image has to be fetched.
*/
@ExperimentalCoroutinesApi
final override fun fetchImageUrl(page: Page): Observable<String> {
return fetchImageUrlFlow(page).asObservable()
}
open fun fetchImageUrlFlow(page: Page): Flow<String> {
return flow {
val response = client.newCall(imageUrlRequestSuspended(page)).await()
emit(
imageUrlParseSuspended(response)
)
}
}
/**
* Returns the request for getting the url to the source image. Override only if it's needed to
* override the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
final override fun imageUrlRequest(page: Page): Request {
return runBlocking { imageUrlRequestSuspended(page) }
}
protected open suspend fun imageUrlRequestSuspended(page: Page): Request {
return GET(page.url, headers)
}
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
final override fun imageUrlParse(response: Response): String {
return runBlocking { imageUrlParseSuspended(response) }
}
protected abstract suspend fun imageUrlParseSuspended(response: Response): String
/**
* Returns an observable with the response of the source image.
*
* @param page the page whose source image has to be downloaded.
*/
@ExperimentalCoroutinesApi
final override fun fetchImage(page: Page): Observable<Response> {
return fetchImageFlow(page).asObservable()
}
open fun fetchImageFlow(page: Page): Flow<Response> {
return flow {
emit(
client.newCallWithProgress(imageRequestSuspended(page), page).await()
)
}
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
final override fun imageRequest(page: Page): Request {
return runBlocking { imageRequestSuspended(page) }
}
protected open suspend fun imageRequestSuspended(page: Page): Request {
return GET(page.imageUrl!!, headers)
}
/**
* Called before inserting a new chapter into database. Use it if you need to override chapter
* fields, like the title or the chapter number. Do not change anything to [manga].
*
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
final override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
runBlocking { prepareNewChapterSuspended(chapter, manga) }
}
open suspend fun prepareNewChapterSuspended(chapter: SChapter, manga: SManga) {
}
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = runBlocking { getFilterListSuspended() }
open suspend fun getFilterListSuspended() = FilterList()
companion object {
data class LicencedException(override val message: String?) : Exception()
}
}
@@ -12,18 +12,20 @@ import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.annoations.Nsfw
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.network.GET 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -47,12 +49,12 @@ 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.dropBlank
import exh.util.ignore import exh.util.ignore
import exh.util.nullIfBlank import exh.util.nullIfBlank
import exh.util.trimAll
import exh.util.trimOrNull import exh.util.trimOrNull
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.net.URLEncoder
import java.util.ArrayList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.CookieJar import okhttp3.CookieJar
@@ -68,16 +70,19 @@ import org.jsoup.nodes.TextNode
import rx.Observable import rx.Observable
import rx.Single import rx.Single
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import java.util.ArrayList
// TODO Consider gallery updating when doing tabbed browsing // TODO Consider gallery updating when doing tabbed browsing
@Nsfw
class EHentai( class EHentai(
override val id: Long, override val id: Long,
val exh: Boolean, val exh: Boolean,
val context: Context val context: Context
) : HttpSource(), LewdSource<EHentaiSearchMetadata, Document>, UrlImportableSource { ) : HttpSource(), MetadataSource<EHentaiSearchMetadata, Document>, UrlImportableSource {
override val metaClass = EHentaiSearchMetadata::class override val metaClass = EHentaiSearchMetadata::class
val domain: String private val domain: String
get() = if (exh) { get() = if (exh) {
"exhentai.org" "exhentai.org"
} else { } else {
@@ -88,9 +93,9 @@ class EHentai(
get() = "https://$domain" get() = "https://$domain"
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = !exh
private val prefs: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy()
/** /**
@@ -103,11 +108,11 @@ class EHentai(
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
it.selectFirst("th") == null && it.selectFirst(".itd") == null it.selectFirst("th") == null && it.selectFirst(".itd") == null
}.map { }.map { body ->
val thumbnailElement = it.selectFirst(".gl1e img, .gl2c .glthumb img") val thumbnailElement = body.selectFirst(".gl1e img, .gl2c .glthumb img")
val column2 = it.selectFirst(".gl3e, .gl2c") val column2 = body.selectFirst(".gl3e, .gl2c")
val linkElement = it.selectFirst(".gl3c > a, .gl2e > div > a") val linkElement = body.selectFirst(".gl3c > a, .gl2e > div > a")
val infoElement = it.selectFirst(".gl3e") val infoElement = body.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 infoElements = infoElement?.select("div")
@@ -142,7 +147,7 @@ class EHentai(
) )
} }
} else { } else {
val tagElement = it.selectFirst(".gl3c > a") val tagElement = body.selectFirst(".gl3c > a")
val tagElements = tagElement.select("div") val tagElements = tagElement.select("div")
tagElements.forEach { element -> tagElements.forEach { element ->
if (element.className() == "gt") { if (element.className() == "gt") {
@@ -172,11 +177,11 @@ class EHentai(
getPageCount(infoElements.getOrNull(5))?.let { length = it } getPageCount(infoElements.getOrNull(5))?.let { length = it }
} else { } else {
val parsedGenre = it.selectFirst(".gl1c div") val parsedGenre = body.selectFirst(".gl1c div")
getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it } getGenre(genreString = parsedGenre?.text()?.nullIfBlank()?.toLowerCase()?.replace(" ", ""))?.let { genre = it }
val info = it.selectFirst(".gl2c") val info = body.selectFirst(".gl2c")
val extraInfo = it.selectFirst(".gl4c") val extraInfo = body.selectFirst(".gl4c")
val infoList = info.select("div div") val infoList = info.select("div div")
@@ -392,7 +397,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): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
searchMangaRequestObservable(page, query, filters).flatMap { searchMangaRequestObservable(page, query, filters).flatMap {
client.newCall(it).asObservableSuccess() client.newCall(it).asObservableSuccess()
@@ -441,14 +446,14 @@ class EHentai(
override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response) override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request { private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
return GET( return GET(
page?.let { page?.let {
addParam(url, "page", Integer.toString(page - 1)) addParam(url, "page", Integer.toString(page - 1))
} ?: url, } ?: url,
additionalHeaders?.let { additionalHeaders?.let { additionalHeadersNotNull ->
val headers = headers.newBuilder() val headers = headers.newBuilder()
it.toMultimap().forEach { (t, u) -> additionalHeadersNotNull.toMultimap().forEach { (t, u) ->
u.forEach { u.forEach {
headers.add(t, it) headers.add(t, it)
} }
@@ -597,12 +602,10 @@ class EHentai(
RaisedTag( RaisedTag(
namespace, namespace,
element.text().trim(), element.text().trim(),
if (element.hasClass("gtl")) { when {
TAG_TYPE_LIGHT element.hasClass("gtl") -> TAG_TYPE_LIGHT
} else if (element.hasClass("gtw")) { element.hasClass("gtw") -> TAG_TYPE_WEAK
TAG_TYPE_WEAK else -> TAG_TYPE_NORMAL
} else {
TAG_TYPE_NORMAL
} }
) )
} }
@@ -627,7 +630,7 @@ class EHentai(
.map { realImageUrlParse(it, page) } .map { realImageUrlParse(it, page) }
} }
fun realImageUrlParse(response: Response, page: Page): String { private fun realImageUrlParse(response: Response, page: Page): String {
with(response.asJsoup()) { with(response.asJsoup()) {
val currentImage = getElementById("img").attr("src") val currentImage = getElementById("img").attr("src")
// Each press of the retry button will choose another server // Each press of the retry button will choose another server
@@ -678,30 +681,30 @@ class EHentai(
} }
fun spPref() = if (exh) { fun spPref() = if (exh) {
prefs.eh_exhSettingsProfile() preferences.eh_exhSettingsProfile()
} else { } else {
prefs.eh_ehSettingsProfile() preferences.eh_ehSettingsProfile()
} }
fun rawCookies(sp: Int): Map<String, String> { private fun rawCookies(sp: Int): Map<String, String> {
val cookies: MutableMap<String, String> = mutableMapOf() val cookies: MutableMap<String, String> = mutableMapOf()
if (prefs.enableExhentai().get()) { if (preferences.enableExhentai().get()) {
cookies[LoginController.MEMBER_ID_COOKIE] = prefs.memberIdVal().get() cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
cookies[LoginController.PASS_HASH_COOKIE] = prefs.passHashVal().get() cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get()
cookies[LoginController.IGNEOUS_COOKIE] = prefs.igneousVal().get() cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get()
cookies["sp"] = sp.toString() cookies["sp"] = sp.toString()
val sessionKey = prefs.eh_settingsKey().get() val sessionKey = preferences.eh_settingsKey().get()
if (sessionKey.isNotBlank()) { if (sessionKey.isNotBlank()) {
cookies["sk"] = sessionKey cookies["sk"] = sessionKey
} }
val sessionCookie = prefs.eh_sessionCookie().get() val sessionCookie = preferences.eh_sessionCookie().get()
if (sessionCookie.isNotBlank()) { if (sessionCookie.isNotBlank()) {
cookies["s"] = sessionCookie cookies["s"] = sessionCookie
} }
val hathPerksCookie = prefs.eh_hathPerksCookies().get() val hathPerksCookie = preferences.eh_hathPerksCookies().get()
if (hathPerksCookie.isNotBlank()) { if (hathPerksCookie.isNotBlank()) {
cookies["hath_perks"] = hathPerksCookie cookies["hath_perks"] = hathPerksCookie
} }
@@ -721,7 +724,7 @@ class EHentai(
// Headers // Headers
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader()) override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader())
fun addParam(url: String, param: String, value: String) = Uri.parse(url) private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
.buildUpon() .buildUpon()
.appendQueryParameter(param, value) .appendQueryParameter(param, value)
.toString() .toString()
@@ -746,9 +749,10 @@ class EHentai(
return FilterList( return FilterList(
AutoCompleteTags( AutoCompleteTags(
EHTags.getNameSpaces().map { "$it:" } + EHTags.getAllTags(), EHTags.getNameSpaces().map { "$it:" } + EHTags.getAllTags(),
EHTags.getNameSpaces().map { "$it:" }, excludePrefix EHTags.getNameSpaces().map { "$it:" },
excludePrefix
), ),
if (prefs.eh_watchedListDefaultState().get()) { if (preferences.eh_watchedListDefaultState().get()) {
Watched(isEnabled = true) Watched(isEnabled = true)
} else { } else {
Watched(isEnabled = false) Watched(isEnabled = false)
@@ -816,14 +820,13 @@ class EHentai(
private fun combineQuery(filters: FilterList): String { private fun combineQuery(filters: FilterList): String {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter -> val advSearch = filters.filterIsInstance<Filter.AutoComplete>().flatMap { filter ->
val splitState = filter.state.map(String::trim).filterNot(String::isBlank) val splitState = filter.state.trimAll().dropBlank()
splitState.mapNotNull { tag -> splitState.mapNotNull { tag ->
val split = tag.split(":").filterNot { it.isBlank() }.toMutableList() val split = tag.split(":").filterNot { it.isBlank() }
if (split.size > 1) { if (split.size > 1) {
val namespace = split[0].removePrefix("-") val namespace = split[0].removePrefix("-")
val exclude = split[0].startsWith("-") val exclude = split[0].startsWith("-")
split -= namespace AdvSearchEntry(Pair(namespace, split[1]), exclude)
AdvSearchEntry(Pair(namespace, split.joinToString(":")), exclude)
} else { } else {
null null
} }
@@ -865,7 +868,7 @@ class EHentai(
UriFilter { UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state > 0) { if (state > 0) {
builder.appendQueryParameter("f_srdd", Integer.toString(state + 1)) builder.appendQueryParameter("f_srdd", (state + 1).toString())
builder.appendQueryParameter("f_sr", "on") builder.appendQueryParameter("f_sr", "on")
} }
} }
@@ -8,7 +8,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -18,19 +18,17 @@ import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HitomiDescriptionAdapter import exh.ui.metadata.adapters.HitomiDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class Hitomi(delegate: HttpSource, val context: Context) : class Hitomi(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<HitomiSearchMetadata, Document>, MetadataSource<HitomiSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val metaClass = HitomiSearchMetadata::class override val metaClass = HitomiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang override val lang = if (id == otherId) "all" else delegate.lang
override val id: Long
get() = if (delegate.lang == "other") otherId else delegate.id
// 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> =
@@ -87,7 +85,8 @@ class Hitomi(delegate: HttpSource, val context: Context) :
characters = content.select("a").map { it.text() } characters = content.select("a").map { it.text() }
tags += characters.map { tags += characters.map {
RaisedTag( RaisedTag(
"character", it, "character",
it,
HitomiSearchMetadata.TAG_TYPE_DEFAULT HitomiSearchMetadata.TAG_TYPE_DEFAULT
) )
} }
@@ -98,7 +97,8 @@ class Hitomi(delegate: HttpSource, val context: Context) :
else if (it.attr("href").startsWith("/tag/female")) "female" else if (it.attr("href").startsWith("/tag/female")) "female"
else "misc" else "misc"
RaisedTag( RaisedTag(
ns, it.text().dropLast(if (ns == "misc") 0 else 2), ns,
it.text().dropLast(if (ns == "misc") 0 else 2),
HitomiSearchMetadata.TAG_TYPE_DEFAULT HitomiSearchMetadata.TAG_TYPE_DEFAULT
) )
} }
@@ -114,7 +114,13 @@ class Hitomi(delegate: HttpSource, val context: Context) :
} }
} }
override fun toString() = "${delegate.name} (${lang.toUpperCase()})" override fun toString() = "$name (${lang.toUpperCase()})"
override fun ensureDelegateCompatible() {
if (versionId != delegate.versionId) {
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId})!")
}
}
override val matchingHosts = listOf( override val matchingHosts = listOf(
"hitomi.la" "hitomi.la"
@@ -1,40 +1,268 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.preference.PreferenceScreen import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.Source
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.online.BrowseSourceFilterHeader
import eu.kanade.tachiyomi.source.online.FollowsSource
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.md.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser
import exh.md.handlers.ApiMangaParser
import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler
import exh.md.utils.FollowStatus
import exh.md.utils.MdLang
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import kotlin.reflect.KClass
class MangaDex(delegate: HttpSource, val context: Context) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
ConfigurableSource, MetadataSource<MangaDexSearchMetadata, Response>,
UrlImportableSource { UrlImportableSource,
FollowsSource,
LoginSource,
BrowseSourceFilterHeader,
RandomMangaSource {
override val lang: String = delegate.lang
override val headers: Headers
get() = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
}
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org") override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
val preferences: PreferencesHelper by injectLazy()
val trackManager: TrackManager by injectLazy()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters) importIdToMdId(query) {
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() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
"/manga/${uri.pathSegments[1]}" MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
} else { } else {
null null
} }
} }
override val lang: String get() = delegate.lang override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return MangaHandler(client, headers, listOf(mdLang), preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) = (delegate as ConfigurableSource).setupPreferenceScreen(screen) override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return MangaHandler(client, headers, listOf(mdLang), preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
}
@ExperimentalSerializationApi
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return if (chapter.scanlator == "MangaPlus") {
client.newCall(mangaPlusPageListRequest(chapter))
.asObservableSuccess()
.map { response ->
val chapterId = ApiChapterParser().externalParse(response)
MangaPlusHandler(client).fetchPageList(chapterId)
}
} else super.fetchPageList(chapter)
}
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
return GET(MdUtil.baseUrl + chpUrl + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
}
override fun fetchImage(page: Page): Observable<Response> {
return if (page.imageUrl!!.contains("mangaplus", true)) {
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
.asObservableSuccess()
} else super.fetchImage(page)
}
override val metaClass: KClass<MangaDexSearchMetadata> = MangaDexSearchMetadata::class
override fun getDescriptionAdapter(controller: MangaController): MangaDexDescriptionAdapter {
return MangaDexDescriptionAdapter(controller)
}
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, preferences.mangaDexForceLatestCovers().get())
}
override fun fetchFollows(): Observable<MangasPage> {
return FollowsHandler(client, headers, Injekt.get()).fetchFollows()
}
override val needsLogin: Boolean = true
override fun getLoginDialog(source: Source, activity: Activity): DialogController {
return MangadexLoginDialog(source as MangaDex)
}
override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
}
override suspend fun login(
username: String,
password: String,
twoFactorCode: String
): Boolean {
return withContext(Dispatchers.IO) {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).execute()
response.body!!.string().isEmpty()
}
}
override suspend fun logout(): Boolean {
return withContext(Dispatchers.IO) {
// https://mangadex.org/ajax/actions.ajax.php?function=logout
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
val listOfDexCookies = network.cookieManager.get(httpUrl)
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
val token = cookie?.value
if (token.isNullOrEmpty()) {
return@withContext true
}
val result = client.newCall(
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
).execute()
val resultStr = result.body!!.string()
if (resultStr.contains("success", true)) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withContext true
}
false
}
}
override fun fetchAllFollows(forceHd: Boolean): Flow<List<Pair<SManga, MangaDexSearchMetadata>>> {
return flow { emit(FollowsHandler(client, headers, Injekt.get()).fetchAllFollows(forceHd)) }
}
fun updateReadingProgress(track: Track): Flow<Boolean> {
return flow { FollowsHandler(client, headers, Injekt.get()).updateReadingProgress(track) }
}
fun updateRating(track: Track): Flow<Boolean> {
return flow { FollowsHandler(client, headers, Injekt.get()).updateRating(track) }
}
override fun fetchTrackingInfo(url: String): Flow<Track> {
return flow {
if (!isLogged()) {
throw Exception("Not Logged in")
}
emit(FollowsHandler(client, headers, Injekt.get()).fetchTrackingInfo(url))
}
}
override fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean> {
return flow { emit(FollowsHandler(client, headers, Injekt.get()).updateFollowStatus(mangaID, followStatus)) }
}
override fun getFilterHeader(controller: Controller): MangaDexFabHeaderAdapter {
return MangaDexFabHeaderAdapter(controller, this)
}
override fun fetchRandomMangaUrl(): Flow<String> {
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
}
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
when {
query.toIntOrNull() != null -> {
Observable.fromCallable {
// MdUtil.
val res = GalleryAdder().addGallery(context, MdUtil.baseUrl + MdUtil.mapMdIdToMangaUrl(query.toInt()), false, this)
MangasPage(
(
if (res is GalleryAddEvent.Success) {
listOf(res.manga)
} else {
emptyList()
}
),
false
)
}
}
else -> fail()
}
companion object {
private const val REMEMBER_ME = "mangadex_rememberme_token"
}
} }
@@ -1,203 +1,189 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.util.Log
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
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.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
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.FilterList import eu.kanade.tachiyomi.source.model.FilterList
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.SuspendHttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.merged.sql.models.MergedMangaReference
import exh.util.asFlow
import exh.util.await import exh.util.await
import hu.akarnokd.rxjava.interop.RxJavaInterop
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.single
import kotlinx.coroutines.rx2.asFlowable import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.rx2.asSingle import kotlinx.coroutines.withContext
import okhttp3.Response import okhttp3.Response
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
// TODO LocalSource compatibility class MergedSource : SuspendHttpSource() {
// TODO Disable clear database option
class MergedSource : HttpSource() {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val gson: Gson by injectLazy() private val downloadManager: DownloadManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
override val id: Long = MERGED_SOURCE_ID override val id: Long = MERGED_SOURCE_ID
override val baseUrl = "" override val baseUrl = ""
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException() override suspend fun popularMangaRequestSuspended(page: Int) = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException() override suspend fun popularMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException() override suspend fun searchMangaRequestSuspended(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException() override suspend fun searchMangaParseSuspended(response: Response) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() override suspend fun latestUpdatesRequestSuspended(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException() override suspend fun latestUpdatesParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun mangaDetailsParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun chapterListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun pageListParseSuspended(response: Response) = throw UnsupportedOperationException()
override suspend fun imageUrlParseSuspended(response: Response) = throw UnsupportedOperationException()
override fun fetchChapterListFlow(manga: SManga) = throw UnsupportedOperationException()
override fun fetchImageFlow(page: Page) = throw UnsupportedOperationException()
override fun fetchImageUrlFlow(page: Page) = throw UnsupportedOperationException()
override fun fetchPageListFlow(chapter: SChapter) = throw UnsupportedOperationException()
override fun fetchLatestUpdatesFlow(page: Int) = throw UnsupportedOperationException()
override fun fetchPopularMangaFlow(page: Int) = throw UnsupportedOperationException()
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetailsFlow(manga: SManga): Flow<SManga> {
return RxJavaInterop.toV1Observable( return flow {
readMangaConfig(manga).load(db, sourceManager).take(1).map { loaded -> val mergedManga = db.getManga(manga.url, id).await() ?: throw Exception("merged manga not in db")
val mangaReferences = mergedManga.id?.let { withContext(Dispatchers.IO) { db.getMergedMangaReferences(it).await() } } ?: throw Exception("merged manga id is null")
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted")
if (mangaReferences.size == 1 || {
val mangaReference = mangaReferences.firstOrNull()
mangaReference == null || (mangaReference.mangaSourceId == MERGED_SOURCE_ID)
}()
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
emit(
SManga.create().apply { SManga.create().apply {
this.copyFrom(loaded.manga) val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
val dbManga = mangaInfoReference?.let { withContext(Dispatchers.IO) { db.getManga(it.mangaUrl, it.mangaSourceId).await() } }
this.copyFrom(dbManga ?: mergedManga)
url = manga.url url = manga.url
} }
}.asFlowable() )
) }
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { fun getChaptersFromDB(manga: Manga, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
return RxJavaInterop.toV1Single( // TODO more chapter dedupe
GlobalScope.async(Dispatchers.IO) { return db.getChaptersByMergedMangaId(manga.id!!).asRxObservable()
val loadedMangas = readMangaConfig(manga).load(db, sourceManager).buffer() .asFlow()
loadedMangas.map { loadedManga -> .map { chapterList ->
async(Dispatchers.IO) { val mangaReferences = withContext(Dispatchers.IO) { db.getMergedMangaReferences(manga.id!!).await() }
loadedManga.source.fetchChapterList(loadedManga.manga).map { chapterList -> val sources = mangaReferences.map { sourceManager.getOrStub(it.mangaSourceId) to it.mangaId }
chapterList.map { chapter -> if (editScanlators) {
chapter.apply { chapterList.onEach { chapter ->
url = writeUrlConfig(UrlConfig(loadedManga.source.id, url, loadedManga.manga.url)) val source = sources.firstOrNull { chapter.manga_id == it.second }?.first
} if (source != null) {
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
else "$source: ${chapter.scanlator}"
}
}
}
if (dedupe) dedupeChapterList(mangaReferences, chapterList) else chapterList
}
}
private fun dedupeChapterList(mangaReferences: List<MergedMangaReference>, chapterList: List<Chapter>): List<Chapter> {
return when (mangaReferences.firstOrNull { it.mangaSourceId == MERGED_SOURCE_ID }?.chapterSortMode) {
MergedMangaReference.CHAPTER_SORT_NO_DEDUPE, MergedMangaReference.CHAPTER_SORT_NONE -> chapterList
MergedMangaReference.CHAPTER_SORT_PRIORITY -> chapterList
MergedMangaReference.CHAPTER_SORT_MOST_CHAPTERS -> {
findSourceWithMostChapters(chapterList)?.let { mangaId ->
chapterList.filter { it.manga_id == mangaId }
} ?: chapterList
}
MergedMangaReference.CHAPTER_SORT_HIGHEST_CHAPTER_NUMBER -> {
findSourceWithHighestChapterNumber(chapterList)?.let { mangaId ->
chapterList.filter { it.manga_id == mangaId }
} ?: chapterList
}
else -> chapterList
}
}
private fun findSourceWithMostChapters(chapterList: List<Chapter>): Long? {
return chapterList.groupBy { it.manga_id }.maxByOrNull { it.value.size }?.key
}
private fun findSourceWithHighestChapterNumber(chapterList: List<Chapter>): Long? {
return chapterList.maxByOrNull { it.chapter_number }?.manga_id
}
fun fetchChaptersForMergedManga(manga: Manga, downloadChapters: Boolean = true, editScanlators: Boolean = false, dedupe: Boolean = true): Flow<List<Chapter>> {
return flow {
withContext(Dispatchers.IO) {
fetchChaptersAndSync(manga, downloadChapters).collect()
}
emit(
getChaptersFromDB(manga, editScanlators, dedupe).singleOrNull() ?: emptyList<Chapter>()
)
}
}
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Flow<Pair<List<Chapter>, List<Chapter>>> {
val mangaReferences = db.getMergedMangaReferences(manga.id!!).await()
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
return mangaReferences.filter { it.mangaSourceId != MERGED_SOURCE_ID }.asFlow().map {
load(db, sourceManager, it)
}.buffer().flatMapMerge { loadedManga ->
withContext(Dispatchers.IO) {
if (loadedManga.manga != null && loadedManga.reference.getChapterUpdates) {
loadedManga.source.fetchChapterList(loadedManga.manga).asFlow()
.map { syncChaptersWithSource(db, it, loadedManga.manga, loadedManga.source) }
.onEach {
if (ifDownloadNewChapters && loadedManga.reference.downloadChapters) {
downloadManager.downloadChapters(loadedManga.manga, it.first)
} }
}.toSingle().await(Schedulers.io()) }
} } else {
}.buffer().map { it.await() }.toList().flatten() emptyList<Pair<List<Chapter>, List<Chapter>>>().asFlow()
}.asSingle(Dispatchers.IO) }
).toObservable() }
}.buffer()
} }
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException() suspend fun load(db: DatabaseHelper, sourceManager: SourceManager, reference: MergedMangaReference): LoadedMangaSource {
override fun chapterListParse(response: Response) = throw UnsupportedOperationException() var manga = db.getManga(reference.mangaUrl, reference.mangaSourceId).await()
val source = sourceManager.getOrStub(manga?.source ?: reference.mangaSourceId)
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { if (manga == null) {
val config = readUrlConfig(chapter.url) manga = Manga.create(reference.mangaSourceId).apply {
val source = sourceManager.getOrStub(config.source) url = reference.mangaUrl
return source.fetchPageList(
SChapter.create().apply {
copyFrom(chapter)
url = config.url
} }
).map { pages -> manga.copyFrom(source.fetchMangaDetails(manga).asFlow().single())
pages.map { page -> try {
page.copyWithUrl(writeUrlConfig(UrlConfig(config.source, page.url, config.mangaUrl))) manga.id = db.insertManga(manga).await().insertedId()
reference.mangaId = manga.id
db.insertNewMergedMangaId(reference).await()
} catch (e: Exception) {
XLog.st(e.stackTrace.contentToString(), 5)
} }
} }
return LoadedMangaSource(source, manga, reference)
} }
override fun fetchImageUrl(page: Page): Observable<String> { data class LoadedMangaSource(val source: Source, val manga: Manga?, val reference: MergedMangaReference)
val config = readUrlConfig(page.url)
val source = sourceManager.getOrStub(config.source) as? HttpSource
?: throw UnsupportedOperationException("This source does not support this operation!")
return source.fetchImageUrl(page.copyWithUrl(config.url))
}
override fun pageListParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun fetchImage(page: Page): Observable<Response> {
val config = readUrlConfig(page.url)
val source = sourceManager.getOrStub(config.source) as? HttpSource
?: throw UnsupportedOperationException("This source does not support this operation!")
return source.fetchImage(page.copyWithUrl(config.url))
}
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
val chapterConfig = readUrlConfig(chapter.url)
val source = sourceManager.getOrStub(chapterConfig.source) as? HttpSource
?: throw UnsupportedOperationException("This source does not support this operation!")
val copiedManga = SManga.create().apply {
this.copyFrom(manga)
url = chapterConfig.mangaUrl
}
chapter.url = chapterConfig.url
source.prepareNewChapter(chapter, copiedManga)
chapter.url = writeUrlConfig(UrlConfig(source.id, chapter.url, chapterConfig.mangaUrl))
chapter.scanlator = if (chapter.scanlator.isNullOrBlank()) source.name
else "$source: ${chapter.scanlator}"
}
fun readMangaConfig(manga: SManga): MangaConfig {
return MangaConfig.readFromUrl(gson, manga.url)
}
fun readUrlConfig(url: String): UrlConfig {
return gson.fromJson(url)
}
fun writeUrlConfig(urlConfig: UrlConfig): String {
return gson.toJson(urlConfig)
}
data class LoadedMangaSource(val source: Source, val manga: Manga)
data class MangaSource(
@SerializedName("s")
val source: Long,
@SerializedName("u")
val url: String
) {
suspend fun load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource? {
val manga = db.getManga(url, source).executeAsBlocking() ?: return null
val source = sourceManager.getOrStub(source)
return LoadedMangaSource(source, manga)
}
}
data class MangaConfig(
@SerializedName("c")
val children: List<MangaSource>
) {
fun load(db: DatabaseHelper, sourceManager: SourceManager): Flow<LoadedMangaSource> {
return children.asFlow().map { mangaSource ->
mangaSource.load(db, sourceManager)
?: run {
XLog.w("> Missing source manga: $mangaSource")
Log.d("MERGED", "> Missing source manga: $mangaSource")
throw IllegalStateException("Missing source manga: $mangaSource")
}
}
}
fun writeAsUrl(gson: Gson): String {
return gson.toJson(this)
}
companion object {
fun readFromUrl(gson: Gson, url: String): MangaConfig {
return gson.fromJson(url)
}
}
}
data class UrlConfig(
@SerializedName("s")
val source: Long,
@SerializedName("u")
val url: String,
@SerializedName("m")
val mangaUrl: String
)
fun Page.copyWithUrl(newUrl: String) = Page(
index,
newUrl,
imageUrl,
uri
)
override val lang = "all" override val lang = "all"
override val supportsLatest = false override val supportsLatest = false
@@ -13,7 +13,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.NHentaiSearchMetadata import exh.metadata.metadata.NHentaiSearchMetadata
@@ -25,14 +25,12 @@ import exh.util.urlImportFetchSearchManga
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
open class NHentai(delegate: HttpSource, val context: Context) : class NHentai(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<NHentaiSearchMetadata, Response>, MetadataSource<NHentaiSearchMetadata, Response>,
UrlImportableSource { UrlImportableSource {
override val metaClass = NHentaiSearchMetadata::class override val metaClass = NHentaiSearchMetadata::class
override val lang = if (delegate.lang == "other") "all" else delegate.lang override val lang = if (id == otherId) "all" else delegate.lang
override val id: Long
get() = if (delegate.lang == "other") otherId else delegate.id
// 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> =
@@ -100,7 +98,13 @@ open class NHentai(delegate: HttpSource, val context: Context) :
} }
} }
override fun toString() = "${delegate.name} (${lang.toUpperCase()})" override fun toString() = "$name (${lang.toUpperCase()})"
override fun ensureDelegateCompatible() {
if (versionId != delegate.versionId) {
throw IncompatibleDelegateException("Delegate source is not compatible (versionId: $versionId <=> ${delegate.versionId})!")
}
}
override val matchingHosts = listOf( override val matchingHosts = listOf(
"nhentai.net" "nhentai.net"
@@ -7,7 +7,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -24,7 +24,7 @@ import rx.Observable
class PervEden(delegate: HttpSource, val context: Context) : class PervEden(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<PervEdenSearchMetadata, Document>, MetadataSource<PervEdenSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val metaClass = PervEdenSearchMetadata::class override val metaClass = PervEdenSearchMetadata::class
override val lang = delegate.lang override val lang = delegate.lang
@@ -77,7 +77,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
artist = it.text() artist = it.text()
tags += RaisedTag( tags += RaisedTag(
"artist", it.text().toLowerCase(), "artist",
it.text().toLowerCase(),
RaisedSearchMetadata.TAG_TYPE_VIRTUAL RaisedSearchMetadata.TAG_TYPE_VIRTUAL
) )
} }
@@ -85,7 +86,8 @@ class PervEden(delegate: HttpSource, val context: Context) :
"Genres" -> { "Genres" -> {
if (it is Element && it.tagName() == "a") { if (it is Element && it.tagName() == "a") {
tags += RaisedTag( tags += RaisedTag(
null, it.text().toLowerCase(), null,
it.text().toLowerCase(),
PervEdenSearchMetadata.TAG_TYPE_DEFAULT PervEdenSearchMetadata.TAG_TYPE_DEFAULT
) )
} }
@@ -7,7 +7,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -22,7 +22,7 @@ import rx.Observable
class EightMuses(delegate: HttpSource, val context: Context) : class EightMuses(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<EightMusesSearchMetadata, Document>, MetadataSource<EightMusesSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val metaClass = EightMusesSearchMetadata::class override val metaClass = EightMusesSearchMetadata::class
override val lang = "en" override val lang = "en"
@@ -65,8 +65,8 @@ class EightMuses(delegate: HttpSource, val context: Context) :
thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull() thumbnailUrl = parseSelf(input).let { it.albums + it.images }.firstOrNull()
?.selectFirst(".lazyload") ?.selectFirst(".lazyload")
?.attr("data-src")?.let { ?.attr("data-src")?.let {
baseUrl + it baseUrl + it
} }
tags.clear() tags.clear()
tags += RaisedTag( tags += RaisedTag(
@@ -7,7 +7,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -22,7 +22,7 @@ import rx.Observable
class HBrowse(delegate: HttpSource, val context: Context) : class HBrowse(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<HBrowseSearchMetadata, Document>, MetadataSource<HBrowseSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val metaClass = HBrowseSearchMetadata::class override val metaClass = HBrowseSearchMetadata::class
override val lang = "en" override val lang = "en"
@@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -24,7 +24,7 @@ import rx.Observable
class HentaiCafe(delegate: HttpSource, val context: Context) : class HentaiCafe(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<HentaiCafeSearchMetadata, Document>, MetadataSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
/** /**
* An ISO 639-1 compliant language code (two letters in lower case). * An ISO 639-1 compliant language code (two letters in lower case).
@@ -7,7 +7,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -24,7 +24,7 @@ import rx.Observable
class Pururin(delegate: HttpSource, val context: Context) : class Pururin(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<PururinSearchMetadata, Document>, MetadataSource<PururinSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
/** /**
* An ISO 639-1 compliant language code (two letters in lower case). * An ISO 639-1 compliant language code (two letters in lower case).
@@ -7,7 +7,7 @@ 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.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.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@@ -20,14 +20,14 @@ import exh.ui.metadata.adapters.TsuminoDescriptionAdapter
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 java.text.SimpleDateFormat
import java.util.Locale
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import rx.Observable import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class Tsumino(delegate: HttpSource, val context: Context) : class Tsumino(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
LewdSource<TsuminoSearchMetadata, Document>, MetadataSource<TsuminoSearchMetadata, Document>,
UrlImportableSource { UrlImportableSource {
override val metaClass = TsuminoSearchMetadata::class override val metaClass = TsuminoSearchMetadata::class
override val lang = "en" override val lang = "en"
@@ -7,11 +7,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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 uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
@@ -49,6 +49,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
when (preferences.themeDark().get()) { when (preferences.themeDark().get()) {
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
else -> R.style.Theme_Tachiyomi_Dark else -> R.style.Theme_Tachiyomi_Dark
} }
} }
@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.base.changehandler
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
/**
* An [AnimatorChangeHandler] that will cross fade two views
*/
class OneWayFadeChangeHandler : AnimatorChangeHandler {
constructor()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration)
constructor(duration: Long, removesFromViewOnPush: Boolean) : super(
duration,
removesFromViewOnPush
)
override fun getAnimator(container: ViewGroup, from: View?, to: View?, isPush: Boolean, toAddedToContainer: Boolean): Animator {
val animator = AnimatorSet()
if (to != null) {
val start: Float = if (toAddedToContainer) 0F else to.alpha
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f))
}
if (from != null && (!isPush || removesFromViewOnPush())) {
container.removeView(from)
}
return animator
}
override fun resetFromView(from: View) {
from.alpha = 1f
}
override fun copy(): ControllerChangeHandler {
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
}
}
@@ -22,27 +22,29 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
lateinit var binding: VB lateinit var binding: VB
init { init {
addLifecycleListener(object : LifecycleListener() { addLifecycleListener(
override fun postCreateView(controller: Controller, view: View) { object : LifecycleListener() {
onViewCreated(view) override fun postCreateView(controller: Controller, view: View) {
} onViewCreated(view)
}
override fun preCreateView(controller: Controller) { override fun preCreateView(controller: Controller) {
Timber.d("Create view for ${controller.instance()}") Timber.d("Create view for ${controller.instance()}")
} }
override fun preAttach(controller: Controller, view: View) { override fun preAttach(controller: Controller, view: View) {
Timber.d("Attach view for ${controller.instance()}") Timber.d("Attach view for ${controller.instance()}")
} }
override fun preDetach(controller: Controller, view: View) { override fun preDetach(controller: Controller, view: View) {
Timber.d("Detach view for ${controller.instance()}") Timber.d("Detach view for ${controller.instance()}")
} }
override fun preDestroyView(controller: Controller, view: View) { override fun preDestroyView(controller: Controller, view: View) {
Timber.d("Destroy view for ${controller.instance()}") Timber.d("Destroy view for ${controller.instance()}")
}
} }
}) )
} }
override val containerView: View? override val containerView: View?
@@ -98,17 +100,19 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
var expandActionViewFromInteraction = false var expandActionViewFromInteraction = false
fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) { fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) {
setOnActionExpandListener(object : MenuItem.OnActionExpandListener { setOnActionExpandListener(
override fun onMenuItemActionExpand(item: MenuItem): Boolean { object : MenuItem.OnActionExpandListener {
return onExpand?.invoke(item) ?: true override fun onMenuItemActionExpand(item: MenuItem): Boolean {
} return onExpand?.invoke(item) ?: true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
return onCollapse?.invoke(item) ?: true return onCollapse?.invoke(item) ?: true
}
} }
}) )
if (expandActionViewFromInteraction) { if (expandActionViewFromInteraction) {
expandActionViewFromInteraction = false expandActionViewFromInteraction = false
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import eu.kanade.tachiyomi.ui.base.changehandler.OneWayFadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean { fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag) val controller = getControllerWithTag(tag)
@@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
} }
} }
fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction { fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this) return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler(duration)) .pushChangeHandler(OneWayFadeChangeHandler())
.popChangeHandler(FadeChangeHandler(duration)) .popChangeHandler(OneWayFadeChangeHandler())
} }
@@ -10,12 +10,12 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import java.util.concurrent.TimeUnit
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
private typealias ExtensionTuple = private typealias ExtensionTuple =
Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>> Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>

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