Compare commits

...

208 Commits

Author SHA1 Message Date
Jobobby04 cbf82a9d6a Hide dedupe by priority 2021-04-11 21:53:50 -04:00
Jobobby04 6fa67c9a5f Release 1.6.0 2021-04-11 21:51:55 -04:00
Jobobby04 7a85d6b163 Update dependancies 2021-04-11 21:43:44 -04:00
Jobobby04 5a909f48b6 Fix some logs 2021-04-11 21:43:05 -04:00
Jobobby04 4d22db919d Disable mangadex tracking 2021-04-11 20:59:34 -04:00
Jobobby04 8a9f2cce10 More inset fixes 2021-04-11 20:56:50 -04:00
Jobobby04 ede0892cda Cleanup and fixes 2021-04-11 20:43:34 -04:00
Jobobby04 5df0eb7ed1 Convert EHentai Login controller to a activity 2021-04-11 20:43:21 -04:00
Jobobby04 67cb42ff30 Some inset fixes 2021-04-11 20:32:44 -04:00
Jobobby04 e65ea94a08 Comment out Mangadex intents 2021-04-11 20:31:12 -04:00
arkon fdac8a0380 Lint fixes/ignore some errors
(cherry picked from commit a3f1b72126)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2021-04-11 18:48:14 -04:00
arkon 1c56624d13 Make library update/backup error log action clearer for non-technical users
(cherry picked from commit a82e5f5452)
2021-04-11 18:47:32 -04:00
arkon 7c05c59501 Add locales: jv, lt, ne
(cherry picked from commit e10cb0e632)
2021-04-11 18:47:23 -04:00
arkon af77a58dcb Update DoH translations
(cherry picked from commit c7e07a6df0)
2021-04-11 18:47:16 -04:00
Jozef Hollý 5ee87ce8fc Weblate translations (#4647)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@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/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
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/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
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/hu/
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/jv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
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/sr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
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/Tachiyomi 0.x

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: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@gmail.com>
(cherry picked from commit 2e0c778090)
2021-04-11 18:47:09 -04:00
arkon 348ef2cf0f Log "Invalid download location" issues to error log
(cherry picked from commit d421401626)
2021-04-11 18:46:45 -04:00
arkon 828944950b Add Google DoH provider
(cherry picked from commit b2d4e5ab84)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2021-04-11 18:46:37 -04:00
Ivan Iskandar 1c67e82325 BrowseSourceController: Fix navigation bar insets not properly applied (#4810)
(cherry picked from commit 84e023607c)
2021-04-11 18:44:02 -04:00
Ken Swenson a45e273e2c Move deletion actions to the IO thread (#4808)
(cherry picked from commit f145fd0dec)
2021-04-11 18:43:55 -04:00
arkon 45cf4adb5b Update some dependencies; downgrade core-ktx
Fixes ActionMode being underneath statusbar

(cherry picked from commit 42a9f911d8)
2021-04-11 18:43:46 -04:00
arkon eb823cb208 Revert manga title folder for saved pages (closes #4803)
People also didn't like it making their galleries more complicate to navigate.

(cherry picked from commit 9567d55312)
2021-04-11 18:43:37 -04:00
arkon 056358fb9d Update to Gradle 7
(cherry picked from commit 531cd99247)
2021-04-11 18:43:27 -04:00
Ivan Iskandar 9e40625c08 Draw edge-to-edge (#4802)
(cherry picked from commit f3660d88dd)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2021-04-11 18:43:19 -04:00
arkon 9684e34241 [SKIP CI] Add lock workflow
(cherry picked from commit 3accb9a08b)
2021-04-11 18:41:47 -04:00
arkon 84fdd097e0 Update some internal dependencies
They no longer rely on jcenter

(cherry picked from commit 63ce7371bb)
2021-04-11 18:41:36 -04:00
Riztard Lanthorn a3c44fc5ad Search in library include manga description (#4787)
Co-Authored-By: jobobby04 <jobobby04@gmail.com>

Co-authored-by: jobobby04 <jobobby04@gmail.com>
(cherry picked from commit 01c3498dbf)
2021-04-11 18:41:28 -04:00
Taco 196e437da5 Update NDK, more KTX usage (#4792)
* Update NDK

* Utilize more KTX extensions

(cherry picked from commit b3471234ad)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
2021-04-11 18:41:20 -04:00
arkon 5e8b5ef6cf Add clarification for category exclusion (closes #4777)
(cherry picked from commit b2d697131c)
2021-04-11 18:40:31 -04:00
arkon 1ba07466ef Minor cleanup
(cherry picked from commit ef49fc91d8)
2021-04-11 18:40:23 -04:00
arkon eb88c9c94b Flip crop borders and orientation toggles
(cherry picked from commit 6222b47a4f)

# Conflicts:
#	app/src/main/res/layout/reader_activity.xml
2021-04-11 18:40:12 -04:00
arkon d6cab9f9a5 Update Kotlin and Kotlinter
(cherry picked from commit f58e3c390a)
2021-04-11 18:39:06 -04:00
arkon bcc120056c Make reader spinner colors a bit more consistent
(cherry picked from commit 7504621a24)

# Conflicts:
#	app/src/main/res/layout/reader_general_settings.xml
2021-04-11 18:38:58 -04:00
arkon 0e8aec7929 Align filter spinners (closes #2995)
(cherry picked from commit 88e49a9b8b)
2021-04-11 18:37:13 -04:00
Jobobby04 2d4e589db8 Search browse if tag clicked in manga from Index controller 2021-04-11 18:36:41 -04:00
Jobobby04 3eecf5cb20 Disable Mangadex delegation 2021-04-11 18:17:06 -04:00
Jobobby04 6b08889c15 Use a Enum for MigrationStatus 2021-04-06 13:39:25 -04:00
Jobobby04 3bf070d88a Use material dialogs for exclude from automatic deletion 2021-04-04 21:40:26 -04:00
arkon 6d9753f361 Revert using fetch date for updates list
Spamming the list post-migration is currently a more common usecase than sources without chapter dates. We'll need to figure out a better way of handling both scenarios.

(cherry picked from commit 5b23f29d06)
2021-04-04 19:12:44 -04:00
arkon f6b9867ce8 Fix global update category exclusion
(cherry picked from commit c1bdebee78)
2021-04-04 19:12:36 -04:00
Riztard Lanthorn 03366ae7e5 add sort by date fetched in library (#4773)
* add sort by date fetched in library

* chapter fetch date to 8

(cherry picked from commit ddd4cc10ff)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt
2021-04-04 19:12:25 -04:00
arkon a70a6cbe49 Allow excluding categories from auto-download
Closes #1412

Supersedes #4121

(cherry picked from commit 0ca62a4acc)
2021-04-04 19:10:51 -04:00
arkon b5a109440f Allow excluding categories from library update
Closes #3467, #4661, #1839

Supersedes #4474

(cherry picked from commit 4f1275ac01)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2021-04-04 19:10:42 -04:00
arkon f6acf9325a Use Material Dialogs for auto-download categories preference
To allow for negative selections in the future.

(cherry picked from commit b2fee7035f)
2021-04-04 19:09:25 -04:00
arkon b0c0b12499 Use Material Dialogs for global update categories preference
To allow for negative selections in the future.

(cherry picked from commit e15d7cb548)
2021-04-04 19:09:16 -04:00
arkon 30ed1f11ee Fix label overflow for reader spinner preferences
(cherry picked from commit 3257cbe21f)
2021-04-04 19:09:07 -04:00
arkon 3077dc24ec Move BiometricUtil to correct package
(cherry picked from commit 1237af1ff3)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt
2021-04-04 19:08:57 -04:00
arkon c2e882cb5b Allow weaker unlock methods (closes #4265)
(cherry picked from commit 68600b337e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt
2021-04-04 19:08:22 -04:00
arkon 835351f206 Use app name for page download folder and use manga title subfolders (closes #4684)
(cherry picked from commit dac2072eaa)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
2021-04-04 19:07:44 -04:00
arkon cee8335518 Make extension load error logs less verbose
(cherry picked from commit 1b921f9845)
2021-04-04 19:07:01 -04:00
arkon 3aa5a36fdd Minor cleanup
(cherry picked from commit a3992d9fbe)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2021-04-04 19:05:54 -04:00
Tooster 74795bcc5e Replace reading mode snackbar with toast (#4752)
(cherry picked from commit efd2a0cb7b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2021-04-04 19:00:54 -04:00
Andreas 38a46825e2 Remove weird cropping from icon when showing missing chapter warning (#4769)
(cherry picked from commit fba428257b)
2021-04-04 18:56:56 -04:00
arkon 7073e9b9e5 Don't repeatedly vibrate/make sounds on download progress
(cherry picked from commit ff36901007)
2021-04-04 18:56:48 -04:00
arkon 620887f90b Add QuadStateCheckBox view
(cherry picked from commit 940d8389b5)
2021-04-04 18:56:39 -04:00
arkon e38a0d47ac Better handle webtoon SSIV crop border change
(cherry picked from commit 7aa379a857)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt
2021-04-04 18:56:31 -04:00
arkon eb9de3e6f1 Add tooltips for previous/next chapter buttons
Based on https://github.com/Jays2Kings/tachiyomiJ2K/commit/d0738f5b00dfc2f6225cf7a20758b61dcb720168

(cherry picked from commit 1657f04d55)
2021-04-04 18:54:49 -04:00
Jobobby04 37d9a51706 Lint 2021-04-04 18:54:18 -04:00
Jobobby04 acb9bafa0a Only enable search when mode is Catalogue 2021-04-04 18:54:07 -04:00
Jobobby04 7c4e89cbc5 Fix crashing when loading a chapter from manual search in migration 2021-03-31 16:40:38 -04:00
Jobobby04 5842765eda Update crashlytics and fast adapter 2021-03-31 14:32:07 -04:00
Jobobby04 0925bd6a37 Use a buffered reader instead of a scanner for custom manga info 2021-03-31 14:31:39 -04:00
Jobobby04 2ddf5f5037 Fix some search bugs when using the latest/browse menu 2021-03-31 14:31:08 -04:00
Jobobby04 367d95c825 Logging fixes and lint 2021-03-31 14:29:27 -04:00
Jobobby04 6951314744 Fix migration getting stopped when opening views under it 2021-03-31 01:23:51 -04:00
Jobobby04 d294db3e4e Continues -> Continuous 2021-03-30 20:00:55 -04:00
arkon b2cf1266ba Recreate webtoon SSIV when crop borders setting changes (fixes #4734)
(cherry picked from commit 407e798fdb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt
2021-03-30 19:24:46 -04:00
arkon fb01b547de Add icon for crop border shortcut off state
(cherry picked from commit 4054f2a6a0)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/res/layout/reader_activity.xml
2021-03-30 19:24:46 -04:00
arkon d3482ef734 Allow translating DNS over HTTPS (closes #4747)
(cherry picked from commit 468cdf603c)
2021-03-30 19:24:45 -04:00
Jobobby04 d622c659eb Fix toggle crop borders button a bit 2021-03-30 19:24:45 -04:00
arkon d1c497aa60 Fix nav overlay always showing on start (fixes #4736)
(cherry picked from commit 988ec6a224)
2021-03-30 19:24:44 -04:00
Andreas 29a882eebb Remove insert page when dual page split get turned off (#4739)
(cherry picked from commit bdbdf211e2)
2021-03-30 19:24:44 -04:00
Johannes Joens 90ffb8cdf6 add support for Repos with Numbers in their name (#255)
* add support for Repos with Numbers in their name

* Update strings_sy.xml

changed invalid_repo_name to better reflect its meaning
2021-03-30 19:23:52 -04:00
Jays2Kings dc760c0596 Backing up custom data for manga
Using 800s from J2k in BackupManga for this(except for status)

(cherry picked from commit c21b91bc026213993a67089ef4bc76c68ade4445)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/CustomMangaManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt
#	app/src/main/res/values/strings.xml
2021-03-28 19:46:11 -04:00
arkon 7be8062a2e Fix binding of intarray preferences (maybe fixes #4728)
(cherry picked from commit 0437703cbf)
2021-03-28 19:10:36 -04:00
arkon de9ce8f949 Use regular crop icon
(cherry picked from commit 71aa592111)
2021-03-28 19:10:27 -04:00
arkon 3c3f5cf35d Add crop borders shortcut
(cherry picked from commit d501c02f8b)

# Conflicts:
#	app/src/main/res/layout/reader_activity.xml
2021-03-28 19:10:15 -04:00
arkon 7407e22b4e Remove ALPHA from dual page split label
(cherry picked from commit 9daf0e78b8)
2021-03-28 19:05:12 -04:00
arkon 3a18e76089 Clean up SpinnerPreference a bit
(cherry picked from commit dfa07a5f35)
2021-03-28 19:05:02 -04:00
arkon fa67ff165e Show nav overlay on invert tap change
Based on https://github.com/Jays2Kings/tachiyomiJ2K/commit/db4eca90e957d70a102aa631708a156c29418bd3

(cherry picked from commit 437c995d12)
2021-03-28 19:04:55 -04:00
mutsumi b9d2591e2a Fix Some Bangumi Track Bug (#4726)
(cherry picked from commit cc6ae9d1a8)
2021-03-28 19:04:46 -04:00
arkon 404a6a621a Prevent manga title from jumping (fixes #4709)
(cherry picked from commit c58e4f4dee)
2021-03-28 19:04:38 -04:00
arkon aa376dc3a5 Show number of manga per source in migrate menu (#4703)
(cherry picked from commit c87b0e77de)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/MigrationSourcesPresenter.kt
2021-03-28 19:04:30 -04:00
arkon 4ee110e225 Dismiss action toolbar after download action in updates (closes #4729)
(cherry picked from commit 355d5af8ae)
2021-03-28 19:01:28 -04:00
arkon 26d52f5ad7 Fix fullscreen not applying on opening reader (fixes #4723)
(cherry picked from commit 3d99a8ebdb)
2021-03-28 19:01:19 -04:00
arkon 8b37c27a73 Cleanup reader spinner layouts
(cherry picked from commit c4b975b777)

# Conflicts:
#	app/src/main/res/layout/reader_general_settings.xml
#	app/src/main/res/layout/reader_webtoon_settings.xml
2021-03-28 19:01:08 -04:00
Antoine Gaudreau Simard 6e9043c633 Add onPause\onResume persistence to searchView. Fixes issue #3627 (#4494)
* Add onPause\onResume persistence to searchView. Fixes issue #3627

* New controller subclass with built-in SearchView support

* Implement new SearchableNucleusController in SourceController

* Add query to BasePresenter (for one field it is not worth create a subclass in my opinion), convert BrowseSourceController to inherit from SearchableNucleusController

* move to flows to fix an issue in GlobalSearch where it would trigger the search multiple times

* Continue conversion to SearchableNucleusController

* Convert LibraryController, convert to flows, Known ISSUE with empty string being posted after setting the query upon creation of UI

* Fix issues with the post being tide to the SearchView queue which is not processed until shown. Add COLLAPSING state capture which should wrap this up.

* refactoring & enforce @StringRes for queryHint

(cherry picked from commit 2911fe7a1a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2021-03-28 18:33:41 -04:00
arkon 2988524fd8 Clean up reader sheet spinner preferences
Based on https://github.com/Jays2Kings/tachiyomiJ2K/commit/fe2543b9d5da176b1dbb95058d1bfc54400fd47a

Co-Authored-By: Jays2Kings
(cherry picked from commit 14c114756d)

# Conflicts:
#	app/src/main/res/layout/reader_general_settings.xml
#	app/src/main/res/layout/reader_pager_settings.xml
#	app/src/main/res/layout/reader_webtoon_settings.xml
2021-03-28 18:01:39 -04:00
arkon 95c828bed6 Reduce height of sheet when on color filter tab
(cherry picked from commit e7a8107279)
2021-03-28 17:53:35 -04:00
arkon 8721d8c9ec Add tooltips to bottom reader menu items
(cherry picked from commit bff73b1b40)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2021-03-28 17:53:25 -04:00
arkon 5b9d2175e2 Reorganize reader sheet contents a bit
(cherry picked from commit c255f57d95)

# Conflicts:
#	app/src/main/res/layout/reader_general_settings.xml
2021-03-28 17:49:40 -04:00
arkon 75f0ab2f40 Split general and reading mode sheet settings
(cherry picked from commit 64c47bbaed)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderGeneralSettings.kt
#	app/src/main/res/layout/reader_general_settings.xml
#	app/src/main/res/layout/reader_pager_settings.xml
#	app/src/main/res/layout/reader_webtoon_settings.xml
2021-03-28 17:47:31 -04:00
arkon 709f76d53d Merge reader settings and color filter sheets
Heavily influenced by https://github.com/Jays2Kings/tachiyomiJ2K/commit/fe2543b9d5da176b1dbb95058d1bfc54400fd47a#diff-8f47d7b7b53769ac18c28fe9978140c6bef44709879567acab2c6ef3270cd3a8

(cherry picked from commit e0b7698d40)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt
#	app/src/main/res/layout/reader_activity.xml
#	app/src/main/res/layout/reader_settings_sheet.xml
2021-03-28 17:19:57 -04:00
arkon ac654340d8 Maybe make opening file picker for choosing backup file more reliable
(cherry picked from commit a01792ac9a)
2021-03-28 16:51:40 -04:00
arkon 438f64a358 Use more common MIME type for protobuf
(cherry picked from commit 3ba078f64c)
2021-03-28 16:51:29 -04:00
arkon 41aec8bc96 Show unread entries first when sorting by unread (closes #4711)
Based on https://github.com/Jays2Kings/tachiyomiJ2K/commit/b212f8233e2d3ceffaddc5fcd1ef884e137dae2a

(cherry picked from commit a16240f123)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
2021-03-28 16:51:17 -04:00
arkon 97342723bf Update plugins
(cherry picked from commit e5a120e778)
2021-03-28 16:50:41 -04:00
Jays2Kings a1cb3afe77 Added Start/Finished Date Support to AniList
Based on https://github.com/Jays2Kings/tachiyomiJ2K/commit/1e3de8a67f239a3126178b95f2b028fbba1e7633

Co-Authored-By: Jays2Kings
(cherry picked from commit 2ba60e9114)
2021-03-28 16:50:33 -04:00
CrepeTF 1165c57ffa Apply vertical seekbar hide logic to ReaderSettingsSheet
(cherry picked from commit 4e8006f329cc87438de9202cf0ac1d0d8ceb203f)
2021-03-22 21:10:23 -04:00
CrepeTF 565f005692 Vertical seekbar options hidden when force horizontal is enabled
(cherry picked from commit 4105d8de5618b4becd724a00070a2c5c43ba9c3c)
2021-03-22 21:07:34 -04:00
Ken Swenson 3a148c73ac Fix migration due to variable shadowing (#4689)
(cherry picked from commit 472ce5a5e4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
2021-03-22 20:31:10 -04:00
Jobobby04 12962b3486 Minor cleanup 2021-03-22 20:25:46 -04:00
Jobobby04 75da7dcbdd Update dependancies 2021-03-22 20:11:43 -04:00
Jobobby04 f02e3ae28f More blocking fixes 2021-03-22 20:11:15 -04:00
arkon c6369ed73f Handle null Anilist start dates (fixes #4685)
(cherry picked from commit 99ba84c810)
2021-03-21 00:07:02 -04:00
arkon fae2bd7ab7 Minor code cleanup
(cherry picked from commit 78285bdf37)
2021-03-21 00:06:54 -04:00
Andreas 03912407d5 Add navigation layout overlay (#4683)
* Add navigation layout overlay

* Minor clean up

Destroy animator when done not on start
Move and change pref title
Add summary

(cherry picked from commit 5a7f2684b3)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt
#	app/src/main/res/layout/reader_activity.xml
#	app/src/main/res/values/colors.xml
2021-03-21 00:06:46 -04:00
arkon 879b41e97d Fix chapters list getting updated from wrong thread (fixes #4505)
(cherry picked from commit d912a42249)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2021-03-21 00:04:17 -04:00
arkon 6c3a957733 Fix Bangumi search null image errors
(cherry picked from commit 6d8c4fb8b1)
2021-03-21 00:02:37 -04:00
arkon 3d7c00c057 Make tapping available extension row prompt install
(cherry picked from commit a63cecbfcb)
2021-03-21 00:02:25 -04:00
arkon 6e1adf6e04 Fix offline restore ignoring manga from not installed sources (fixes #4679)
(cherry picked from commit 4a5bceb4e4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt
2021-03-21 00:02:16 -04:00
arkon 23091cf50a Update AGP
(cherry picked from commit 86541445b7)
2021-03-21 00:01:11 -04:00
Ken Swenson 78d49b0742 Implement migration for source search (#4657)
(cherry picked from commit b6e6f490e9)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
2021-03-20 15:40:14 -04:00
scb261 30250e350f Limit query for recent chapters to 500 (#4678)
(cherry picked from commit 2145e878a4)
2021-03-20 14:57:30 -04:00
CrepeTF d9b3b7b266 Add option to force disable vertical seekbar
(cherry picked from commit b5df33bf14d4eea8421d2e1e6b488b79e6daa9f5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
#	app/src/main/java/exh/log/Logging.kt
2021-03-20 14:56:51 -04:00
Jobobby04 5558790e15 Fix Sync Favorites 2021-03-19 15:16:42 -04:00
Jobobby04 a1a9b4b812 Lint 2021-03-18 21:55:25 -04:00
Jobobby04 aac2fcb7d4 Catch more mangadex exceptions 2021-03-18 19:48:06 -04:00
KokaKiwi 69ddd04256 [SKIP CI] Update README.md (#4667)
Fix link to Code of Conduct.

(cherry picked from commit 355f6db255)
2021-03-18 17:16:01 -04:00
Jobobby04 7624abbebd Close mangadex login response body 2021-03-18 17:11:28 -04:00
Jobobby04 67310ada53 Move depercated logging to the bottom of the file 2021-03-18 17:06:19 -04:00
Jobobby04 aa73670d50 Fix MDList not getting max chapter/marked as completed 2021-03-18 17:05:18 -04:00
Soitora 2bde782211 [SKIP CI] Add Code of Conduct (#4665)
* Add Code of Conduct

* Update badge section

* Add Code of Conduct link to README

* Change to relative links

(cherry picked from commit bc7632bf02)

# Conflicts:
#	README.md
2021-03-18 15:42:07 -04:00
arkon 7b01f0c608 Add icons for reading mode toggle
(cherry picked from commit 609d8c9685)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
#	app/src/main/res/layout/reader_activity.xml
2021-03-18 15:40:12 -04:00
arkon 781f4e393e Less janky enum iteration
(cherry picked from commit 2f08515455)
2021-03-18 15:36:26 -04:00
scb261 93c92b674d Use fetch date instead of upload date when querying recent chapters (#4645)
(cherry picked from commit 7f450e185d)
2021-03-18 15:36:17 -04:00
arkon 368f565942 Remove __cfduid cookie check
As per email:

Cloudflare is deprecating the __cfduid cookie and the cf-request-id headers. The __cfduid cookie will be removed on 10 May 2021 and the cf-request-id headers will be removed on 1 July. We expect that most customers will not have to take action as a result of this removal. [...] Starting on 10 May 2021, we will stop adding a “Set-Cookie” header on all HTTP responses. The last __cfduid cookies will expire 30 days after that.

(cherry picked from commit 747879b4ec)
2021-03-18 15:36:08 -04:00
Riztard Lanthorn 01c298bbc1 Library update freq: add 4 & 8 hours (#4557)
(cherry picked from commit 4193870fa6)
2021-03-18 15:36:01 -04:00
arkon 1399042efb Flip order of previous chapter reader transition text (closes #4608)
(cherry picked from commit cdc5de3f1b)
2021-03-18 15:35:50 -04:00
arkon b2bfccdeae Round snackbar corners
(cherry picked from commit bc34d4fa88)
2021-03-18 15:35:41 -04:00
arkon 0d46e00b31 Adjust reader navigation button ripples
(cherry picked from commit 6fd4af8736)
2021-03-18 15:35:33 -04:00
arkon 9aca115977 Refactor LibraryUpdateService a bit for future changes
(cherry picked from commit b5c2934270)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2021-03-18 15:35:21 -04:00
arkon e31e71ad44 Remove online protobuf backup restore option
(cherry picked from commit 94f5117941)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt
2021-03-18 15:20:30 -04:00
arkon df950219f5 Use Material dialogs for preferences
Partially addresses #2907

(cherry picked from commit 112e233498)
2021-03-18 15:11:13 -04:00
arkon 23e4b661bc Tweak dialog corner radius
(cherry picked from commit 18b1326f3a)
2021-03-18 15:11:02 -04:00
arkon 7164f686d4 Add reading mode toggle
(cherry picked from commit 1e58b05ead)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/res/layout/reader_activity.xml
2021-03-18 15:10:53 -04:00
arkon 3122f783a9 Move reader setting related classes
(cherry picked from commit 938919bd9b)
2021-03-18 14:55:52 -04:00
arkon 6be8e2de3c Move clear history from advanced settings to history screen menu (closes #4613)
(cherry picked from commit b6b78994d8)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2021-03-18 14:55:42 -04:00
arkon c092127404 Add "my" locale
(cherry picked from commit fddd8ce305)
2021-03-18 14:54:03 -04:00
Jozef Hollý e7dd5f3c25 Weblate translations (#4461)
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
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: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@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/bn/
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/fa/
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/ka/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
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/uz/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
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: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
(cherry picked from commit ccff337975)
2021-03-18 14:53:54 -04:00
arkon 142fc0e4a6 Disable sensor when using force orientation (closes #4618)
(cherry picked from commit fde6b7af4f)
2021-03-18 14:53:41 -04:00
arkon 300e04e8f6 Allow scrolling within reader color filter sheet (fixes #4612)
(cherry picked from commit 0657db7dcb)
2021-03-18 14:53:30 -04:00
Soitora 07f684ac9e Update URL for Local Manga guide (#4641)
(cherry picked from commit d1c2eaf6d5)
2021-03-18 14:52:54 -04:00
arkon 6840382df2 Dependency updates
(cherry picked from commit 91bb6b9016)
2021-03-18 14:52:29 -04:00
Jobobby04 c7b6216d24 Fix recursive call 2021-03-13 11:59:14 -05:00
Jobobby04 a989426d95 Sync Follows sync status choice 2021-03-12 18:22:21 -05:00
Jobobby04 d255ee805b Mdlist only set the status to reading if Unfollowed 2021-03-12 10:02:47 -05:00
Jobobby04 21240cad06 Cleanup 2021-03-11 22:39:46 -05:00
Jobobby04 5b8b10a96b Fix full backup restore locking up 2021-03-11 22:39:18 -05:00
Jobobby04 c600d45e84 Maybe fix EHentai dupes in browse issue 2021-03-11 22:39:17 -05:00
Jobobby04 e9fd6ab470 Revert "Experimental Backup Restore fix"
This reverts commit 3d507600cb.
2021-03-11 19:44:36 -05:00
Jobobby04 3d507600cb Experimental Backup Restore fix 2021-03-11 19:10:34 -05:00
Jobobby04 84abe044a3 Remove Hentai Cafe Delegation 2021-03-11 19:10:33 -05:00
Jobobby04 04200bb590 Cleanup 2021-03-11 19:10:33 -05:00
Jobobby04 42d49b7cba Log tracking errors 2021-03-11 19:10:32 -05:00
Jobobby04 5dace4fd74 Fix Mangadex Login, Fix Mangadex tracking, Set Mangadex track status to Reading on tracked 2021-03-11 19:10:32 -05:00
Jobobby04 ccdae6bb9a Deprecate throwable logging function, produces bad log 2021-03-11 19:10:31 -05:00
Jobobby04 984956ce95 Fix vertical reader scrollbar buttons cutoff 2021-03-11 19:10:31 -05:00
Jobobby04 0fd9b2a8f6 Fix migration not running sometimes 2021-03-11 19:10:31 -05:00
OncePunchedMan 39f4949189 Tweak "Hot Pink" theme (#239)
* fix backdrop

* remove unused line
2021-03-11 14:32:05 -05:00
OncePunchedMan f7d52e0372 Added "Hot Pink" theme (#238)
* first test

* added hot pink theme

* moved string to correct place
2021-03-10 19:28:07 -05:00
Riztard Lanthorn 6cad8411fe swap webview and filter (#235) 2021-03-08 22:44:31 -05:00
arkon f35abccfd9 Revert to core-ktx:1.5.0-beta01
Fixes bottom reader menu from being hidden behind navbar on Android 5.0.

(cherry picked from commit 90351c6e9e)
2021-03-07 23:21:37 -05:00
Jobobby04 f3573d16b4 Fix #154.6, downloading a merge in the library will properly download 2021-03-07 23:17:43 -05:00
Jobobby04 e6f288e2c9 Fix some cherry pick issues 2021-03-07 22:33:38 -05:00
arkon 833bd6e655 Automatically reopen issues when valid
(cherry picked from commit dd4740e54f)
2021-03-07 13:40:17 -05:00
inorichi 4a30c68cfc Fix a decoder crash with RAR files
(cherry picked from commit 48e7cbd76c)
2021-03-07 13:40:02 -05:00
arkon 346bd5f57a Hide subtitle in migration list of sources if no language set (i.e. uninstalled source)
(cherry picked from commit ae42f59102)
2021-03-07 13:39:41 -05:00
arkon c2e3b4d35a AndroidX dependency updates
(cherry picked from commit aa5861d3ca)
2021-03-07 13:38:41 -05:00
Andreas 1fdb03f7db Dual page split allow to have different setting for Paged and Webtoon (#4527)
(cherry picked from commit 7a64bf55cb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderSettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonConfig.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsReaderController.kt
2021-03-07 13:38:17 -05:00
Jobobby04 da3681e602 Dont throw a exception if the request fails for mdlist tracking 2021-03-07 13:04:53 -05:00
Jobobby04 d64a8907eb Many small changes
- Remove unused gridlayout dependency
- Add RECIEVE_BOOT permission for EH updater
- Some suspending db IO calls
2021-03-07 02:47:48 -05:00
Jobobby04 7e91ae02f1 Upgrade logging, now maps timber to XLog, new logging functions 2021-03-07 00:23:23 -05:00
Jobobby04 9457b832fc Surface errors when saving/sharing covers 2021-03-04 19:10:08 -05:00
Jobobby04 d0561705fe Clear db now has a option to keep read manga 2021-03-04 19:10:07 -05:00
Jobobby04 3601968342 Cleanup data saver 2021-03-04 19:10:07 -05:00
Jobobby04 fa2cde79ba Add errors to browse + latest 2021-03-04 19:10:06 -05:00
Jobobby04 1827fe0ce1 Fix merged manga library download button trying to download from localsource 2021-03-04 19:10:06 -05:00
Jobobby04 3447e0c237 Custom status fix for clean titles 2021-03-04 19:10:05 -05:00
Jobobby04 4f9ae9cc75 Fix clear db not effecting merged manga 2021-03-04 19:10:05 -05:00
Jobobby04 cd1c6cbc89 Add manga with pages read to backup 2021-03-04 19:10:04 -05:00
Jobobby04 66cd4c9b40 Move custom theme strings to strings_sy 2021-03-04 19:10:04 -05:00
CrepeTF 2e1cf49d99 Reader PR (with vertical sidebar) (#216)
* Reader PR

* Dealt with conflicts + updates

* Adeed missing import
2021-03-04 19:08:40 -05:00
curche 0c150694e7 Change Reader settings layout (#231)
* Change Reader settings layout

This commit changes the way the Reader settings are displayed. The
fork specific settings for the reader have been moved to the bottom
instead of being sandwiched between settings from the main app.
Makes it look a better organised now

* restore Cts Vertical to before in Reader settings

the current layout of the Reader settings is thus
  - Reader/Defaults/Meta
  - Display
  - Reading
  - Paged
  - Webtoon
  - Continuous vertical
  - Navigation
  - Fork Settings

Changes made based on review at PR https://github.com/jobobby04/TachiyomiSY/pull/231
2021-02-28 13:33:29 -05:00
scb261 a4c10394b6 Split transition animation setting for webtoon and pager (#230)
* Split transition animation setting for webtoon and pager

* Move variables

* Rename config variables back
2021-02-26 14:51:51 -05:00
curche f78836dac4 Change Similar manga settings layout (#228)
* convert Credit string to strings_sy element

* remove redundant similar screen title in Similar Manga settings
2021-02-25 14:52:44 -05:00
Eugene c88de1ab1b Russian localization (#215)
* Russian localization

* Delete "translatable"

* almost not translated

* more fix

* fix 2

* add and fix 3

* fix 4

* Move Themes and rolback strings main
2021-02-24 18:07:55 -05:00
Jobobby04 9694c8310c Make sure some toasts are used in the main thread 2021-02-24 17:26:08 -05:00
inorichi 1b09eecfce Fix a decoder crash
(cherry picked from commit d4c9ab793f)
2021-02-24 17:16:12 -05:00
inorichi 853e8faec5 Support CMYK and YCCK JPEGs and fix bad PNG cropping
(cherry picked from commit 48d2849d97)
2021-02-24 17:16:05 -05:00
Andreas cfd2d43f1c Let users invert dual page split (#4470)
* Let users invert dual page split

* Use Activity lifecycleScope and cleanup invert logic

(cherry picked from commit 776610d0e6)
2021-02-24 17:15:44 -05:00
Andreas 1d3542b648 Add Right and Left to reader settings (#4489)
* Add Right and Left to settings

* Fix whoopsie and minor tweak to how the array is fetched

(cherry picked from commit 3a790f3d66)
2021-02-24 17:15:33 -05:00
arkon 6dc7b9de92 Add Twitter link to About section
(cherry picked from commit 7382042288)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/more/AboutController.kt
2021-02-24 17:15:23 -05:00
arkon 48a63e26f3 Add orientation toggle to bottom reader menu (not really)
(cherry picked from commit 33992d80bf)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt
#	app/src/main/res/layout/reader_activity.xml
2021-02-24 17:12:14 -05:00
arkon 33b1c93949 Reword bookmark strings to clarify it's for a chapter, not a page
(cherry picked from commit a92b0e567b)
2021-02-24 17:02:44 -05:00
arkon 7a115d8080 Adjust reader seekbar design
- Revert back to old prev/next chapter icons
- Make views taller for easier actions
- Use more consistent spacing
- Add ripples to prev/next chapter buttons

(cherry picked from commit 829a65e515)
2021-02-24 17:02:12 -05:00
arkon 9be7c5e6e1 Initial adoption of bottom reader menus from TachiyomiSY
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
Co-authored-by: CrepeTF <CrepeTF@users.noreply.github.com>
(cherry picked from commit 89837e4ced)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/res/layout/reader_activity.xml
#	app/src/main/res/menu/reader.xml
2021-02-24 17:02:03 -05:00
arkon e38d1dfdc4 [SKIP CI] Add instructions on how to get crash logs in issue templates
(cherry picked from commit 03ad48c055)
2021-02-24 16:45:17 -05:00
arkon f1f993bf38 Rename drawable with more consistent naming
(cherry picked from commit ace1db21d1)
2021-02-24 16:44:46 -05:00
arkon 2845d8cc98 Allow clicking the toolbar to go to the manga
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
(cherry picked from commit 8bb69c455b)
2021-02-24 16:44:20 -05:00
Jobobby04 0185d5f7d6 Fixes for browse + latest page 2021-02-24 16:18:13 -05:00
Jobobby04 079ca1d0b3 Small cleanup 2021-02-24 16:16:47 -05:00
Jobobby04 5a67d8169d Edit manga status + edit local manga fixes 2021-02-24 16:15:19 -05:00
Jobobby04 f1cb4c38a2 Fix Hentai Cafe and 8Muses with the new split extension 2021-02-15 19:35:50 -05:00
Jobobby04 50a5ec45b3 Do a bit of optimization and cleanup, remove old EH startup code 2021-02-14 21:24:26 -05:00
Jobobby04 f76216c038 Revert Jdk 11 update 2021-02-12 20:06:07 -05:00
arkon d55692dc0d [SKIP CI] Update to issue-closer-action@v2.0
(cherry picked from commit f4dd150b70)
2021-02-12 19:50:42 -05:00
arkon ded8f15913 Switch back to new image decoder for preview builds
(cherry picked from commit 2b35d22e25)
2021-02-12 19:50:31 -05:00
Jobobby04 845dbbfa1e Revert "Hide dedupe by priority"
This reverts commit 1a12caa487.
2021-02-12 19:49:51 -05:00
328 changed files with 8437 additions and 4385 deletions
+3 -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.5.0) - I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
@@ -24,3 +24,5 @@ I acknowledge that:
## Other details ## Other details
Additional details and attachments. Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+3 -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.5.0) - I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
@@ -34,3 +34,5 @@ This happened instead.
## Other details ## Other details
Additional details and attachments. Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
+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.5.0) - I have updated to the latest version of the app (stable is v1.6.0)
- I have updated all extensions - I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
@@ -32,10 +32,10 @@ jobs:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 1.8
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 1.8
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
+27 -28
View File
@@ -7,31 +7,30 @@ jobs:
autoclose: autoclose:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Autoclose when created in wrong repo - name: Autoclose issues
uses: arkon/issue-closer-action@v1.1 uses: arkon/issue-closer-action@v3.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title rules: |
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*" [
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned." {
- name: Autoclose when no short description provided "type": "title",
uses: arkon/issue-closer-action@v1.1 "regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
with: "message": "It was not opened in the correct repo, as the template mentioned."
repo-token: ${{ secrets.GITHUB_TOKEN }} },
type: title {
regex: ".*<Write short description here>*" "type": "title",
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title." "regex": ".*<Write short description here>*",
- name: Autoclose when body acknowledgement section not removed "message": "The description in the title was not filled out."
uses: arkon/issue-closer-action@v1.1 },
with: {
repo-token: ${{ secrets.GITHUB_TOKEN }} "type": "body",
type: body "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*" "message": "The acknowledgment section was not removed."
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed." },
- name: Autoclose when body requested information not filled out {
uses: arkon/issue-closer-action@v1.1 "type": "body",
with: "regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
repo-token: ${{ secrets.GITHUB_TOKEN }} "message": "Requested information in the template was not filled out."
type: body }
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*" ]
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
+19
View File
@@ -0,0 +1,19 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 * * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '2'
pr-lock-inactive-days: '2'
+76
View File
@@ -0,0 +1,76 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+7 -2
View File
@@ -1,6 +1,6 @@
| Preview Builds | Release Builds | Tachiyomi Support Server | | Preview Builds | Release Builds | Tachiyomi Support Server |
|-------|----------|----------| |-------|----------|----------|
| [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) | | [![Preview](https://github.com/jobobby04/TachiyomiSYPreview/workflows/Remote%20Dispatch%20Build%20App/badge.svg)](https://github.com/jobobby04/TachiyomiSYPreview/releases) | [![stable release](https://img.shields.io/github/release/jobobby04/tachiyomisy.svg?maxAge=3600&label=download)](https://github.com/jobobby04/tachiyomisy/releases/latest) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY # ![app icon](./.github/readme-images/app-icon.png)TachiyomiSY
@@ -109,7 +109,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
<details><summary>Contributing</summary> <details><summary>Contributing</summary>
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md). See [CONTRIBUTING.md](./CONTRIBUTING.md).
</details>
<details><summary>Code of Conduct</summary>
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
</details> </details>
## FAQ ## FAQ
+25 -27
View File
@@ -34,8 +34,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 = 13 versionCode = 14
versionName = "1.5.0" versionName = "1.6.0"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -95,6 +95,7 @@ android {
exclude("META-INF/LICENSE") exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt") exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE") exclude("META-INF/NOTICE")
exclude("META-INF/*.kotlin_module")
// Compatibility for two RxJava versions (EXH) // Compatibility for two RxJava versions (EXH)
exclude("META-INF/rxjava.properties") exclude("META-INF/rxjava.properties")
@@ -126,20 +127,20 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1") implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries // AndroidX libraries
implementation("androidx.annotation:annotation:1.2.0-beta01") implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation("androidx.appcompat:appcompat:1.3.0-beta01") implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02") implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0") implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2") implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0") implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-beta01") implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.multidex:multidex:2.0.1") implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01") implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.3.0-rc01" val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@@ -150,7 +151,7 @@ dependencies {
// UI library // UI library
implementation("com.google.android.material:material:1.3.0") implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.2") "standardImplementation"("com.google.firebase:firebase-core:18.0.3")
// ReactiveX // ReactiveX
implementation("io.reactivex:rxandroid:1.2.1") implementation("io.reactivex:rxandroid:1.2.1")
@@ -159,7 +160,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0") implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client // Network client
val okhttpVersion = "4.10.0-RC1" val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion") implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@@ -193,7 +194,7 @@ dependencies {
implementation("io.requery:sqlite-android:3.33.0") implementation("io.requery:sqlite-android:3.33.0")
// Preferences // Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3") implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
// Model View Presenter // Model View Presenter
val nucleusVersion = "3.0.0" val nucleusVersion = "3.0.0"
@@ -204,14 +205,12 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440") implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library // Image library
val glideVersion = "4.11.0" val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion") kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219") implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
// Logging // Logging
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
@@ -229,7 +228,8 @@ dependencies {
implementation("eu.davidea:flexible-adapter-ui:1.0.0") implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0") implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d") implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices // 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1" val materialDialogsVersion = "3.1.1"
@@ -242,7 +242,7 @@ dependencies {
implementation("com.bluelinelabs:conductor-support:2.1.5") { implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support") exclude(group = "com.android.support")
} }
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1") implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding // FlowBinding
val flowbindingVersion = "0.12.0" val flowbindingVersion = "0.12.0"
@@ -256,7 +256,7 @@ dependencies {
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}") implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests // Tests
testImplementation("junit:junit:4.13.1") testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1") testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19") testImplementation("org.mockito:mockito-core:1.10.19")
@@ -285,11 +285,11 @@ dependencies {
implementation ("info.debatty:java-string-similarity:2.0.0") implementation ("info.debatty:java-string-similarity:2.0.0")
// Firebase (EH) // Firebase (EH)
implementation("com.google.firebase:firebase-analytics-ktx:18.0.0") implementation("com.google.firebase:firebase-analytics-ktx:18.0.3")
implementation("com.google.firebase:firebase-crashlytics-ktx:17.3.0") implementation("com.google.firebase:firebase-crashlytics-ktx:17.4.1")
// Better logging (EH) // Better logging (EH)
implementation("com.elvishew:xlog:1.7.1") implementation("com.elvishew:xlog:1.9.0")
// Debug utils (EH) // Debug utils (EH)
val debugOverlayVersion = "1.1.3" val debugOverlayVersion = "1.1.3"
@@ -302,12 +302,10 @@ dependencies {
implementation ("me.zhanghai.android.materialratingbar:library:1.4.0") implementation ("me.zhanghai.android.materialratingbar:library:1.4.0")
// JsonReader for similar manga // JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.11.0") implementation("com.squareup.moshi:moshi:1.12.0")
implementation("androidx.gridlayout:gridlayout:1.0.0") implementation("com.mikepenz:fastadapter:5.4.0")
// SY <--
implementation("com.mikepenz:fastadapter:5.3.4")
// SY -->
} }
tasks { tasks {
+7 -2
View File
@@ -14,6 +14,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- For managing extensions --> <!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -149,6 +150,10 @@
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="exh.ui.login.EhLoginActivity"
android:label="EHentaiLogin" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -315,7 +320,7 @@
android:scheme="https" /> android:scheme="https" />
<!-- MangaDex --> <!-- MangaDex -->
<data <!--<data
android:scheme="https" android:scheme="https"
android:host="www.mangadex.org" android:host="www.mangadex.org"
android:pathPrefix="/manga/" /> android:pathPrefix="/manga/" />
@@ -364,7 +369,7 @@
<data <data
android:scheme="https" android:scheme="https"
android:host="www.mangadex.cc" android:host="www.mangadex.cc"
android:pathPrefix="/chapter/" /> android:pathPrefix="/chapter/" />-->
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
+21 -51
View File
@@ -22,7 +22,6 @@ 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.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
@@ -36,11 +35,11 @@ import exh.log.CrashlyticsPrinter
import exh.log.EHDebugModeOverlay import exh.log.EHDebugModeOverlay
import exh.log.EHLogLevel import exh.log.EHLogLevel
import exh.log.EnhancedFilePrinter import exh.log.EnhancedFilePrinter
import exh.log.XLogTree
import exh.log.xLogD
import exh.log.xLogE
import exh.syDebugVersion import exh.syDebugVersion
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -51,7 +50,6 @@ import java.security.Security
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import kotlin.concurrent.thread
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.days import kotlin.time.days
@@ -59,12 +57,11 @@ open class App : Application(), LifecycleObserver {
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
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
Timber.plant(XLogTree()) // SY Redirect Timber to XLog
if (!BuildConfig.DEBUG) addAnalytics() if (!BuildConfig.DEBUG) addAnalytics()
workaroundAndroid7BrokenSSL() workaroundAndroid7BrokenSSL()
@@ -78,7 +75,6 @@ open class App : Application(), LifecycleObserver {
setupNotificationChannels() setupNotificationChannels()
Realm.init(this) Realm.init(this)
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (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()
} }
@@ -105,23 +101,22 @@ open class App : Application(), LifecycleObserver {
try { try {
SSLContext.getInstance("TLSv1.2") SSLContext.getInstance("TLSv1.2")
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e) xLogE("Could not install Android 7 broken SSL workaround!", e)
} }
try { try {
ProviderInstaller.installIfNeeded(applicationContext) ProviderInstaller.installIfNeeded(applicationContext)
} catch (e: GooglePlayServicesRepairableException) { } catch (e: GooglePlayServicesRepairableException) {
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e) xLogE("Could not install Android 7 broken SSL workaround!", e)
} catch (e: GooglePlayServicesNotAvailableException) { } catch (e: GooglePlayServicesNotAvailableException) {
XLog.tag("Init").e("Could not install Android 7 broken SSL workaround!", e) xLogE("Could not install Android 7 broken SSL workaround!", e)
} }
} }
} }
private fun addAnalytics() { private fun addAnalytics() {
firebaseAnalytics = Firebase.analytics
if (syDebugVersion != "0") { if (syDebugVersion != "0") {
firebaseAnalytics.setUserProperty("preview_version", syDebugVersion) Firebase.analytics.setUserProperty("preview_version", syDebugVersion)
} }
} }
@@ -137,36 +132,13 @@ open class App : Application(), LifecycleObserver {
Notifications.createChannels(this) Notifications.createChannels(this)
} }
// EXH
private fun deleteOldMetadataRealm() {
val config = RealmConfiguration.Builder()
.name("gallery-metadata.realm")
.schemaVersion(3)
.deleteRealmIfMigrationNeeded()
.build()
Realm.deleteRealm(config)
// Delete old paper db files
listOf(
File(filesDir, "gallery-ex"),
File(filesDir, "gallery-perveden"),
File(filesDir, "gallery-nhentai")
).forEach {
if (it.exists()) {
thread {
it.deleteRecursively()
}
}
}
}
// EXH // EXH
private fun setupExhLogging() { private fun setupExhLogging() {
EHLogLevel.init(this) EHLogLevel.init(this)
val logLevel = when { val logLevel = when {
EHLogLevel.shouldLog(EHLogLevel.EXTRA) -> LogLevel.ALL EHLogLevel.shouldLog(EHLogLevel.EXTREME) -> LogLevel.ALL
BuildConfig.DEBUG -> LogLevel.DEBUG EHLogLevel.shouldLog(EHLogLevel.EXTRA) || BuildConfig.DEBUG -> LogLevel.DEBUG
else -> LogLevel.WARN else -> LogLevel.WARN
} }
@@ -188,9 +160,8 @@ open class App : Application(), LifecycleObserver {
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
printers += EnhancedFilePrinter printers += EnhancedFilePrinter
.Builder(logFolder.absolutePath) .Builder(logFolder.absolutePath) {
.fileNameGenerator( fileNameGenerator = object : DateFileNameGenerator() {
object : DateFileNameGenerator() {
override fun generateFileName(logLevel: Int, timestamp: Long): String { override fun generateFileName(logLevel: Int, timestamp: Long): String {
return super.generateFileName( return super.generateFileName(
logLevel, logLevel,
@@ -198,13 +169,12 @@ open class App : Application(), LifecycleObserver {
) + "-${BuildConfig.BUILD_TYPE}.log" ) + "-${BuildConfig.BUILD_TYPE}.log"
} }
} }
) flattener { timeMillis, level, tag, message ->
.flattener { timeMillis, level, tag, message -> "${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message"
"${dateFormat.format(timeMillis)} ${LogLevel.getShortLevelName(level)}/$tag: $message" }
cleanStrategy = FileLastModifiedCleanStrategy(7.days.toLongMilliseconds())
backupStrategy = NeverBackupStrategy()
} }
.cleanStrategy(FileLastModifiedCleanStrategy(7.days.toLongMilliseconds()))
.backupStrategy(NeverBackupStrategy())
.build()
// Install Crashlytics in prod // Install Crashlytics in prod
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
@@ -216,8 +186,8 @@ open class App : Application(), LifecycleObserver {
*printers.toTypedArray() *printers.toTypedArray()
) )
XLog.tag("Init").d("Application booting...") xLogD("Application booting...")
XLog.tag("Init").disableStackTrace().d( xLogD(
"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" + "App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})\n" +
"Preview build: $syDebugVersion\n" + "Preview build: $syDebugVersion\n" +
"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" + "Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}) \n" +
@@ -242,7 +212,7 @@ open class App : Application(), LifecycleObserver {
.install() .install()
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
// Crashes if app is in background // Crashes if app is in background
XLog.tag("Init").e("Failed to initialize debug overlay, app in background?", e) xLogE("Failed to initialize debug overlay, app in background?", e)
} }
} }
} }
@@ -9,6 +9,7 @@ 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.updater.UpdaterJob import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@@ -129,6 +130,17 @@ object Migrations {
context.toast(R.string.myanimelist_relogin) context.toast(R.string.myanimelist_relogin)
} }
} }
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
return true return true
} }
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@@ -23,6 +24,10 @@ abstract class AbstractBackupManager(protected val context: Context) {
internal val trackManager: TrackManager by injectLazy() internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy() protected val preferences: PreferencesHelper by injectLazy()
// SY -->
protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <--
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
/** /**
@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.library.CustomMangaManager
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.chapter.NoChaptersException
@@ -24,6 +25,10 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
protected val db: DatabaseHelper by injectLazy() protected val db: DatabaseHelper by injectLazy()
protected val trackManager: TrackManager by injectLazy() protected val trackManager: TrackManager by injectLazy()
// SY -->
protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <--
var job: Job? = null var job: Job? = null
protected lateinit var backupManager: T protected lateinit var backupManager: T
@@ -30,7 +30,12 @@ class BackupCreateService : Service() {
internal const val BACKUP_HISTORY_MASK = 0x4 internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8 internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8 internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
// SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_ALL = 0x1F
// SY <--
/** /**
* Returns the status of the service. * Returns the status of the service.
@@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false) setAutoCancel(false)
setOngoing(true) setOngoing(true)
setOnlyAlertOnce(true)
} }
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) { private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
@@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.creating_backup)) setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true) setProgress(0, 0, true)
setOnlyAlertOnce(true)
} }
builder.show(Notifications.ID_BACKUP_PROGRESS) builder.show(Notifications.ID_BACKUP_PROGRESS)
@@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log), context.getString(R.string.action_show_errors),
NotificationReceiver.openErrorLogPendingActivity(context, uri) NotificationReceiver.openErrorLogPendingActivity(context, uri)
) )
} }
@@ -43,12 +43,11 @@ class BackupRestoreService : Service() {
* @param context context of application * @param context context of application
* @param uri path of Uri * @param uri path of Uri
*/ */
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) { fun start(context: Context, uri: Uri, mode: Int) {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply { val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_MODE, mode) putExtra(BackupConst.EXTRA_MODE, mode)
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
@@ -119,13 +118,12 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL) val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
// Cancel any previous job if needed. // Cancel any previous job if needed.
backupRestore?.job?.cancel() backupRestore?.job?.cancel()
backupRestore = when (mode) { backupRestore = when (mode) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online) BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
else -> LegacyBackupRestore(this, notifier) else -> LegacyBackupRestore(this, notifier)
} }
@@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATE
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
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
@@ -29,11 +31,8 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.util.lang.launchIO
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.JsonSavedSearch
@@ -164,7 +163,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/ */
private fun backupMangaObject(manga: Manga, options: Int): BackupManga { private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
// Entry for this manga // Entry for this manga
val mangaObject = BackupManga.copyFrom(manga) val mangaObject = BackupManga.copyFrom(manga /* SY --> */, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null /* SY <-- */)
// SY --> // SY -->
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
@@ -237,24 +236,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
/** /**
* Fetches manga information * Fetches manga information
* *
* @param source source of manga
* @param manga manga that needs updating * @param manga manga that needs updating
* @return Updated manga info. * @return Updated manga info.
*/ */
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga { fun restoreManga(manga: Manga): Manga {
return if (online && source != null /* SY --> */ && source !is MergedSource /* SY <-- */) { return manga.also {
val networkManga = source.getMangaDetails(manga.toMangaInfo()) it.initialized = it.description != null
manga.also { it.id = insertManga(it)
it.copyFrom(networkManga.toSManga())
it.favorite = manga.favorite
it.initialized = true
it.id = insertManga(manga)
}
} else {
manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
} }
} }
@@ -363,29 +351,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val trackToUpdate = mutableListOf<Track>() val trackToUpdate = mutableListOf<Track>()
tracks.forEach { track -> tracks.forEach { track ->
val service = trackManager.getService(track.sync_id) var isInDatabase = false
if (service != null && service.isLogged) { for (dbTrack in dbTracks) {
var isInDatabase = false if (track.sync_id == dbTrack.sync_id) {
for (dbTrack in dbTracks) { // The sync is already in the db, only update its fields
if (track.sync_id == dbTrack.sync_id) { if (track.media_id != dbTrack.media_id) {
// The sync is already in the db, only update its fields dbTrack.media_id = track.media_id
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
} }
if (!isInDatabase) { }
// Insert new sync. Let the db assign the id if (!isInDatabase) {
track.id = null // Insert new sync. Let the db assign the id
trackToUpdate.add(track) track.id = null
} trackToUpdate.add(track)
} }
} }
// Update database // Update database
@@ -394,47 +379,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
/** internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
* Restore the chapters for manga if chapters already in database
*
* @param manga manga of chapters
* @param chapters list containing chapters that get restored
* @return boolean answering if chapter fetch is not needed
*/
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false
}
chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
chapter.read = dbChapter.read
chapter.last_page_read = dbChapter.last_page_read
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
chapter.last_page_read = dbChapter.last_page_read
}
if (!chapter.bookmark && dbChapter.bookmark) {
chapter.bookmark = dbChapter.bookmark
}
}
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter -> chapters.forEach { chapter ->
@@ -527,8 +472,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
internal suspend fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) { internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
manga.id?.let { mangaId -> val mangaId = manga.id ?: return
launchIO {
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let { databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let {
if (it == null) { if (it == null) {
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId) val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
@@ -15,8 +15,7 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.source.online.all.MergedSource
import exh.EXHMigrations import exh.EXHMigrations
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import okio.buffer import okio.buffer
@@ -24,7 +23,7 @@ import okio.gzip
import okio.source import okio.source
import java.util.Date import java.util.Date
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) { class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override suspend fun performRestore(uri: Uri): Boolean { override suspend fun performRestore(uri: Uri): Boolean {
// SY --> // SY -->
@@ -57,9 +56,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
return false return false
} }
restoreManga(it, backup.backupCategories, online) restoreManga(it, backup.backupCategories)
} }
// TODO: optionally trigger online library + tracker update
return true return true
} }
@@ -81,8 +82,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
} }
// SY <-- // SY <--
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) { private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
var manga = backupManga.getMangaImpl() val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl() val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories val categories = backupManga.categories
val history = backupManga.history val history = backupManga.history
@@ -90,22 +91,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
// SY --> // SY -->
val mergedMangaReferences = backupManga.mergedMangaReferences val mergedMangaReferences = backupManga.mergedMangaReferences
val flatMetadata = backupManga.flatMetadata val flatMetadata = backupManga.flatMetadata
val customManga = backupManga.getCustomMangaInfo()
// SY <-- // SY <--
// SY --> // SY -->
manga = EXHMigrations.migrateBackupEntry(manga) EXHMigrations.migrateBackupEntry(manga)
// SY <-- // SY <--
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try { try {
if (source != null || !online) { restoreMangaData(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online)
} else {
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
} }
@@ -117,35 +113,35 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* Returns a manga restore observable * Returns a manga restore observable
* *
* @param manga manga data from json * @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json * @param chapters chapters data from json
* @param categories categories data from json * @param categories categories data from json
* @param history history data from json * @param history history data from json
* @param tracks tracking data from json * @param tracks tracking data from json
*/ */
private suspend fun restoreMangaData( private fun restoreMangaData(
manga: Manga, manga: Manga,
source: Source?,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
online: Boolean customManga: CustomMangaManager.MangaJson?,
// SY -->
) { ) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction { db.inTransaction {
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) { if (dbManga == null) {
// Manga not in database // Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online) restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
} else { // Manga in database } else {
// Manga in database
// Copy information from manga already in database // Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga) backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information // Fetch rest of manga information
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata, online) restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
} }
} }
} }
@@ -157,66 +153,60 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* @param chapters chapters of manga that needs updating * @param chapters chapters of manga that needs updating
* @param categories categories that need updating * @param categories categories that need updating
*/ */
private suspend fun restoreMangaFetch( private fun restoreMangaFetch(
source: Source?,
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
online: Boolean customManga: CustomMangaManager.MangaJson?,
// SY <--
) { ) {
try { try {
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online) val fetchedManga = backupManager.restoreManga(manga)
fetchedManga.id ?: return fetchedManga.id ?: return
backupManager.restoreChaptersForManga(fetchedManga, chapters)
if (online && source != null) { restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories /* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
// SY -->
if (source !is MergedSource) {
updateChapters(source, fetchedManga, chapters)
}
// SY <--
} else {
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
}
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) { } catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}") errors.add(Date() to "${manga.title} - ${e.message}")
} }
} }
private suspend fun restoreMangaNoFetch( private fun restoreMangaNoFetch(
source: Source?,
backupManga: Manga, backupManga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<Track>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>, mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?, flatMetadata: BackupFlatMetadata?,
online: Boolean customManga: CustomMangaManager.MangaJson?,
// SY <--
) { ) {
if (online && source != null) { backupManager.restoreChaptersForManga(backupManga, chapters)
if (/* SY --> */ source !is MergedSource && /* SY <-- */ !backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
} else {
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories, mergedMangaReferences, flatMetadata) restoreExtraForManga(backupManga, categories, history, tracks, backupCategories/* SY --> */, mergedMangaReferences, flatMetadata, customManga/* SY <-- */)
updateTracking(backupManga, tracks)
} }
private suspend fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>, mergedMangaReferences: List<BackupMergedMangaReference>, flatMetadata: BackupFlatMetadata?) { private fun restoreExtraForManga(
manga: Manga,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
// SY -->
mergedMangaReferences: List<BackupMergedMangaReference>,
flatMetadata: BackupFlatMetadata?,
customManga: CustomMangaManager.MangaJson?,
// SY <--
) {
// Restore categories // Restore categories
backupManager.restoreCategoriesForManga(manga, categories, backupCategories) backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
@@ -232,6 +222,10 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
// Restore flat metadata for metadata sources // Restore flat metadata for metadata sources
flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) } flatMetadata?.let { backupManager.restoreFlatMetadata(manga, it) }
// Restore Custom Info
customManga?.id = manga.id!!
customManga?.let { customMangaManager.saveMangaInfo(it) }
// SY <-- // SY <--
} }
} }
@@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@@ -36,7 +37,15 @@ data class BackupManga(
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(), @ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
// SY specific values // SY specific values
@ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(), @ProtoNumber(600) var mergedMangaReferences: List<BackupMergedMangaReference> = emptyList(),
@ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null @ProtoNumber(601) var flatMetadata: BackupFlatMetadata? = null,
@ProtoNumber(602) var customStatus: Int = 0,
// J2K specific values
@ProtoNumber(800) var customTitle: String? = null,
@ProtoNumber(801) var customArtist: String? = null,
@ProtoNumber(802) var customAuthor: String? = null,
@ProtoNumber(803) var customDescription: String? = null,
@ProtoNumber(803) var customGenre: List<String>? = null
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl().apply { return MangaImpl().apply {
@@ -62,6 +71,29 @@ data class BackupManga(
} }
} }
// SY -->
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
if (customTitle != null ||
customArtist != null ||
customAuthor != null ||
customDescription != null ||
customGenre != null ||
customStatus != 0
) {
return CustomMangaManager.MangaJson(
id = 0L,
title = customTitle,
author = customAuthor,
artist = customArtist,
description = customDescription,
genre = customGenre,
status = customStatus.takeUnless { it == 0 }
)
}
return null
}
// SY <--
fun getTrackingImpl(): List<TrackImpl> { fun getTrackingImpl(): List<TrackImpl> {
return tracking.map { return tracking.map {
it.getTrackingImpl() it.getTrackingImpl()
@@ -69,22 +101,35 @@ data class BackupManga(
} }
companion object { companion object {
fun copyFrom(manga: Manga): BackupManga { fun copyFrom(manga: Manga /* SY --> */, customMangaManager: CustomMangaManager?/* SY <-- */): BackupManga {
return BackupManga( return BackupManga(
url = manga.url, url = manga.url,
title = manga.title, // SY -->
artist = manga.artist, title = manga.originalTitle,
author = manga.author, artist = manga.originalArtist,
description = manga.description, author = manga.originalAuthor,
genre = manga.getGenres() ?: emptyList(), description = manga.originalDescription,
status = manga.status, genre = manga.getOriginalGenres() ?: emptyList(),
status = manga.originalStatus,
// SY <--
thumbnailUrl = manga.thumbnail_url, thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite, favorite = manga.favorite,
source = manga.source, source = manga.source,
dateAdded = manga.date_added, dateAdded = manga.date_added,
viewer = manga.viewer, viewer = manga.viewer,
chapterFlags = manga.chapter_flags chapterFlags = manga.chapter_flags
) // SY -->
).also { backupManga ->
customMangaManager?.getManga(manga)?.let {
backupManga.customTitle = it.title
backupManga.customArtist = it.artist
backupManga.customAuthor = it.author
backupManga.customDescription = it.description
backupManga.customGenre = it.getGenres()
backupManga.customStatus = it.status
}
}
// SY <--
} }
} }
} }
@@ -91,7 +91,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
// SY <-- // SY <--
private suspend fun restoreManga(mangaJson: JsonObject) { private suspend fun restoreManga(mangaJson: JsonObject) {
/* SY --> */ var /* SY <-- */ manga = backupManager.parser.fromJson<MangaImpl>( val manga = backupManager.parser.fromJson<MangaImpl>(
mangaJson.get( mangaJson.get(
Backup.MANGA Backup.MANGA
) )
@@ -114,7 +114,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
) )
// EXH --> // EXH -->
manga = EXHMigrations.migrateBackupEntry(manga) EXHMigrations.migrateBackupEntry(manga)
// <-- EXH // <-- EXH
val source = backupManager.sourceManager.get(manga.source) val source = backupManager.sourceManager.get(manga.source)
@@ -59,8 +59,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_DESCRIPTION to obj.originalDescription, COL_DESCRIPTION to obj.originalDescription,
COL_GENRE to obj.originalGenre, COL_GENRE to obj.originalGenre,
COL_TITLE to obj.originalTitle, COL_TITLE to obj.originalTitle,
COL_STATUS to obj.originalStatus,
// SY <-- // SY <--
COL_STATUS to obj.status,
COL_THUMBNAIL_URL to obj.thumbnail_url, COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite, COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update, COL_LAST_UPDATE to obj.last_update,
@@ -40,9 +40,11 @@ open class MangaImpl : Manga {
override var genre: String? override var genre: String?
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
set(value) { ogGenre = value } set(value) { ogGenre = value }
// SY <--
override var status: Int = 0 override var status: Int
get() = if (favorite) customMangaManager.getManga(this)?.status?.takeUnless { it == 0 } ?: ogStatus else ogStatus
set(value) { ogStatus = value }
// SY <--
override var thumbnail_url: String? = null override var thumbnail_url: String? = null
@@ -71,6 +73,8 @@ open class MangaImpl : Manga {
private set private set
var ogGenre: String? = null var ogGenre: String? = null
private set private set
var ogStatus: Int = 0
private set
// SY <-- // SY <--
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.CategoryTable
@@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
.`object`(manga) .`object`(manga)
.withPutResolver(MangaMigrationPutResolver()) .withPutResolver(MangaMigrationPutResolver())
.prepare() .prepare()
fun updateMangaThumbnail(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaThumbnailPutResolver())
.prepare()
// SY <-- // SY <--
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
@@ -151,12 +157,38 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE})") .where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
)
""".trimIndent()
)
.whereArgs(0) .whereArgs(0)
.build() .build()
) )
.prepare() .prepare()
// SY -->
fun deleteMangasNotInLibraryAndNotRead() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
)
""".trimIndent()
)
.whereArgs(0)
.build()
)
.prepare()
// SY <--
fun deleteMangas() = db.delete() fun deleteMangas() = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
@@ -195,6 +227,16 @@ interface MangaQueries : DbProvider {
) )
.prepare() .prepare()
fun getChapterFetchDateManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
)
.prepare()
// SY --> // SY -->
fun getMangaWithMetadata() = db.get() fun getMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
@@ -69,7 +69,7 @@ fun getReadMangaNotInLibraryQuery() =
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN( WHERE ${Manga.COL_FAVORITE} = 0 AND ${Manga.COL_ID} IN(
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} FROM ${Chapter.TABLE} WHERE ${Chapter.COL_READ} = 1 OR ${Chapter.COL_LAST_PAGE_READ} != 0
) )
""" """
@@ -221,6 +221,16 @@ fun getLatestChapterMangaQuery() =
ORDER by max DESC ORDER by max DESC
""" """
fun getChapterFetchDateMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
/** /**
* Query to get the categories for a manga. * Query to get the categories for a manga.
*/ */
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.database.resolvers package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
@@ -9,6 +8,7 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
import exh.util.nullIfZero
class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() { class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
@@ -31,15 +31,20 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
MangaTable.COL_GENRE to manga.originalGenre, MangaTable.COL_GENRE to manga.originalGenre,
MangaTable.COL_AUTHOR to manga.originalAuthor, MangaTable.COL_AUTHOR to manga.originalAuthor,
MangaTable.COL_ARTIST to manga.originalArtist, MangaTable.COL_ARTIST to manga.originalArtist,
MangaTable.COL_DESCRIPTION to manga.originalDescription MangaTable.COL_DESCRIPTION to manga.originalDescription,
MangaTable.COL_STATUS to manga.originalStatus
) )
fun resetToContentValues(manga: Manga) = ContentValues(1).apply { private fun resetToContentValues(manga: Manga) = contentValuesOf(
val splitter = "▒ ▒∩▒" MangaTable.COL_TITLE to manga.title.split(splitter).last(),
put(MangaTable.COL_TITLE, manga.title.split(splitter).last()) MangaTable.COL_GENRE to manga.genre?.split(splitter)?.lastOrNull(),
put(MangaTable.COL_GENRE, manga.genre?.split(splitter)?.lastOrNull()) MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
put(MangaTable.COL_AUTHOR, manga.author?.split(splitter)?.lastOrNull()) MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
put(MangaTable.COL_ARTIST, manga.artist?.split(splitter)?.lastOrNull()) MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
put(MangaTable.COL_DESCRIPTION, manga.description?.split(splitter)?.lastOrNull()) MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull()
)
companion object {
const val splitter = "▒ ▒∩▒"
} }
} }
@@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
// SY
class MangaThumbnailPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url
)
}
@@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -23,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
*/ */
class DownloadManager(/* SY private */ val context: Context) { class DownloadManager(private val context: Context) {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
@@ -211,16 +212,16 @@ class DownloadManager(/* SY private */ val context: Context) {
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters) val filteredChapters = getChaptersToDelete(chapters)
launchIO {
removeFromDownloadQueue(filteredChapters)
removeFromDownloadQueue(filteredChapters) val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source) cache.removeChapters(filteredChapters, manga)
chapterDirs.forEach { it.delete() } if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
cache.removeChapters(filteredChapters, manga) chapterDirs.firstOrNull()?.parentFile?.delete()
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty }
chapterDirs.firstOrNull()?.parentFile?.delete()
} }
return filteredChapters return filteredChapters
} }
@@ -302,9 +303,11 @@ class DownloadManager(/* SY private */ val context: Context) {
* @param source the source of the manga. * @param source the source of the manga.
*/ */
fun deleteManga(manga: Manga, source: Source) { fun deleteManga(manga: Manga, source: Source) {
downloader.queue.remove(manga) launchIO {
provider.findMangaDir(manga, source)?.delete() downloader.queue.remove(manga)
cache.removeManga(manga) provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
}
} }
/** /**
@@ -27,6 +27,9 @@ internal class DownloadNotifier(private val context: Context) {
private val progressNotificationBuilder by lazy { private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) { context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
} }
} }
@@ -84,7 +87,6 @@ internal class DownloadNotifier(private val context: Context) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
setAutoCancel(false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@@ -127,7 +129,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.chapter_paused)) setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
setAutoCancel(false)
setProgress(0, 0, false) setProgress(0, 0, false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
@@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions() clearActions()
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
@@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
return downloadsDir return downloadsDir
.createDirectory(getSourceDirName(source)) .createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga)) .createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) { } catch (e: Throwable) {
Timber.w(e) Timber.e(e, "Invalid download directory")
throw Exception(context.getString(R.string.invalid_download_dir)) throw Exception(context.getString(R.string.invalid_download_dir))
} }
} }
@@ -8,7 +8,6 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.util.Scanner
class CustomMangaManager(val context: Context) { class CustomMangaManager(val context: Context) {
@@ -23,7 +22,7 @@ class CustomMangaManager(val context: Context) {
val json = try { val json = try {
Json.decodeFromString<MangaList>( Json.decodeFromString<MangaList>(
Scanner(editJson).useDelimiter("\\Z").next() editJson.bufferedReader().use { it.readText() }
) )
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -32,30 +31,15 @@ class CustomMangaManager(val context: Context) {
val mangasJson = json.mangas ?: return mutableMapOf() val mangasJson = json.mangas ?: return mutableMapOf()
return mangasJson.mapNotNull { mangaJson -> return mangasJson.mapNotNull { mangaJson ->
val id = mangaJson.id ?: return@mapNotNull null val id = mangaJson.id ?: return@mapNotNull null
val manga = MangaImpl().apply { id to mangaJson.toManga()
this.id = id
title = mangaJson.title ?: ""
author = mangaJson.author
artist = mangaJson.artist
description = mangaJson.description
genre = mangaJson.genre?.joinToString(", ")
}
id to manga
}.toMap().toMutableMap() }.toMap().toMutableMap()
} }
fun saveMangaInfo(manga: MangaJson) { fun saveMangaInfo(manga: MangaJson) {
if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null) { if (manga.title == null && manga.author == null && manga.artist == null && manga.description == null && manga.genre == null && manga.status == null) {
customMangaMap.remove(manga.id!!) customMangaMap.remove(manga.id!!)
} else { } else {
customMangaMap[manga.id!!] = MangaImpl().apply { customMangaMap[manga.id!!] = manga.toManga()
id = manga.id
title = manga.title ?: ""
author = manga.author
artist = manga.artist
description = manga.description
genre = manga.genre?.joinToString(", ")
}
} }
saveCustomInfo() saveCustomInfo()
} }
@@ -75,7 +59,8 @@ class CustomMangaManager(val context: Context) {
author, author,
artist, artist,
description, description,
genre?.split(", ") genre?.split(", "),
status
) )
} }
@@ -86,24 +71,23 @@ class CustomMangaManager(val context: Context) {
@Serializable @Serializable
data class MangaJson( data class MangaJson(
val id: Long? = null, var id: Long? = null,
val title: String? = null, val title: String? = null,
val author: String? = null, val author: String? = null,
val artist: String? = null, val artist: String? = null,
val description: String? = null, val description: String? = null,
val genre: List<String>? = null val genre: List<String>? = null,
val status: Int? = null
) { ) {
override fun equals(other: Any?): Boolean { fun toManga() = MangaImpl().apply {
if (this === other) return true id = this@MangaJson.id
if (javaClass != other?.javaClass) return false title = this@MangaJson.title ?: ""
other as MangaJson author = this@MangaJson.author
if (id != other.id) return false artist = this@MangaJson.artist
return true description = this@MangaJson.description
} genre = this@MangaJson.genre?.joinToString(", ")
status = this@MangaJson.status ?: 0
override fun hashCode(): Int {
return id.hashCode()
} }
} }
} }
@@ -111,7 +111,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent) setContentIntent(errorLogIntent)
addAction( addAction(
R.drawable.ic_folder_24dp, R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log), context.getString(R.string.action_show_errors),
errorLogIntent errorLogIntent
) )
} }
@@ -41,7 +41,7 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.source.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
@@ -88,6 +88,7 @@ class LibraryUpdateService(
private lateinit var notifier: LibraryUpdateNotifier private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope private lateinit var ioScope: CoroutineScope
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
private var updateJob: Job? = null private var updateJob: Job? = null
/** /**
@@ -109,6 +110,8 @@ class LibraryUpdateService(
companion object { companion object {
private var instance: LibraryUpdateService? = null
/** /**
* Key for category to update. * Key for category to update.
*/ */
@@ -147,7 +150,7 @@ class LibraryUpdateService(
* @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 /* SY --> */, group: Int = LibraryGroup.BY_DEFAULT, groupExtra: String? = null /* SY <-- */): 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)) { return 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) }
@@ -158,10 +161,11 @@ class LibraryUpdateService(
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
return true true
} else {
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target)
false
} }
return false
} }
/** /**
@@ -198,6 +202,9 @@ class LibraryUpdateService(
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
} }
if (instance == this) {
instance = null
}
super.onDestroy() super.onDestroy()
} }
@@ -221,23 +228,27 @@ class LibraryUpdateService(
val target = intent.getSerializableExtra(KEY_TARGET) as? Target val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY ?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed. instance = this
// Unsubscribe from any previous subscription if needed
updateJob?.cancel() updateJob?.cancel()
// Update favorite manga. Destroy service when completed or in case of an error. // Update favorite manga
val selectedScheme = preferences.libraryUpdatePrioritization().get() val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
val mangaList = getMangaToUpdate(intent, target) val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
.sortedWith(rankingScheme[selectedScheme]) val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
addMangaToQueue(categoryId, group, groupExtra, target)
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception) Timber.e(exception)
stopSelf(startId) stopSelf(startId)
} }
updateJob = ioScope.launch(handler) { updateJob = ioScope.launch(handler) {
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers(mangaList) Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings()
// SY --> // SY -->
Target.SYNC_FOLLOWS -> syncFollows() Target.SYNC_FOLLOWS -> syncFollows()
Target.PUSH_FAVORITES -> pushFavorites() Target.PUSH_FAVORITES -> pushFavorites()
@@ -250,36 +261,40 @@ class LibraryUpdateService(
} }
/** /**
* Returns the list of manga to be updated. * Adds list of manga to be updated.
* *
* @param intent the update intent. * @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update. * @param target the target to update.
* @return a list of manga to update
*/ */
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?, target: Target) {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val libraryManga = db.getLibraryMangas().executeAsBlocking()
// SY --> // SY -->
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get() val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
// SY <-- // SY <--
var listToUpdate = if (categoryId != -1) { var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
// SY --> // SY -->
} else if (group == LibraryGroup.BY_DEFAULT || groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.GLOBAL || (groupLibraryUpdateType == PreferenceValues.GroupLibraryMode.ALL_BUT_UNGROUPED && group == LibraryGroup.UNGROUPED)) { } 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()) { val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking() libraryManga.filter { it.category in categoriesToUpdate }
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
} else { } else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } libraryManga
} }
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToExclude }
} else {
emptyList()
}
listToInclude.minus(listToExclude)
} else { } else {
val libraryManga = db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
when (group) { when (group) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val trackingExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1 val trackingExtra = groupExtra?.toIntOrNull() ?: -1
libraryManga.filter { libraryManga.filter {
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
val status: String = run { val status: String = run {
@@ -298,12 +313,12 @@ class LibraryUpdateService(
} }
} }
LibraryGroup.BY_SOURCE -> { LibraryGroup.BY_SOURCE -> {
val sourceExtra = intent.getStringExtra(KEY_GROUP_EXTRA).nullIfBlank() val sourceExtra = groupExtra.nullIfBlank()
val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra } val source = sourceManager.getCatalogueSources().find { it.name == sourceExtra }
if (source != null) libraryManga.filter { it.source == source.id } else emptyList() if (source != null) libraryManga.filter { it.source == source.id } else emptyList()
} }
LibraryGroup.BY_STATUS -> { LibraryGroup.BY_STATUS -> {
val statusExtra = intent.getStringExtra(KEY_GROUP_EXTRA)?.toIntOrNull() ?: -1 val statusExtra = groupExtra?.toIntOrNull() ?: -1
libraryManga.filter { libraryManga.filter {
it.status == statusExtra it.status == statusExtra
} }
@@ -314,10 +329,13 @@ class LibraryUpdateService(
// SY <-- // SY <--
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
} }
return listToUpdate val selectedScheme = preferences.libraryUpdatePrioritization().get()
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
} }
/** /**
@@ -329,7 +347,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update * @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) { suspend fun updateChapterList() {
val semaphore = Semaphore(5) val semaphore = Semaphore(5)
val progressCount = AtomicInteger(0) val progressCount = AtomicInteger(0)
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
@@ -463,7 +481,7 @@ class LibraryUpdateService(
return syncChaptersWithSource(db, chapters, manga, source) return syncChaptersWithSource(db, chapters, manga, source)
} }
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) { private suspend fun updateCovers() {
var progressCount = 0 var progressCount = 0
mangaToUpdate.forEach { manga -> mangaToUpdate.forEach { manga ->
@@ -496,7 +514,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a * Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here. * background thread, so it's safe to do heavy operations or network calls here.
*/ */
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) { private suspend fun updateTrackings() {
var progressCount = 0 var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged } val loggedServices = trackManager.services.filter { it.isLogged }
@@ -539,11 +557,12 @@ class LibraryUpdateService(
private suspend fun syncFollows() { private suspend fun syncFollows() {
val count = AtomicInteger(0) val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager) ?: return
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
val size: Int val size: Int
mangaDex.fetchAllFollows(true) mangaDex.fetchAllFollows(true)
.filter { (_, metadata) -> .filter { (_, metadata) ->
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int syncFollowStatusInts.contains(metadata.follow_status)
} }
.also { size = it.size } .also { size = it.size }
.forEach { (networkManga, metadata) -> .forEach { (networkManga, metadata) ->
@@ -569,7 +588,7 @@ class LibraryUpdateService(
val id = db.insertManga(dbManga).executeOnIO().insertedId() val id = db.insertManga(dbManga).executeOnIO().insertedId()
if (id != null) { if (id != null) {
metadata.mangaId = id metadata.mangaId = id
db.insertFlatMetadata(metadata.flatten()).await() db.insertFlatMetadataAsync(metadata.flatten()).await()
} }
} }
@@ -74,7 +74,7 @@ class NotificationReceiver : BroadcastReceiver() {
shareFile( shareFile(
context, context,
intent.getParcelableExtra(EXTRA_URI), intent.getParcelableExtra(EXTRA_URI),
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip", if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
) )
ACTION_CANCEL_RESTORE -> cancelRestore( ACTION_CANCEL_RESTORE -> cancelRestore(
@@ -17,13 +17,21 @@ object PreferenceKeys {
const val rotation = "pref_rotation_type_key" const val rotation = "pref_rotation_type_key"
const val enableTransitions = "pref_enable_transitions_key" const val enableTransitionsPager = "pref_enable_transitions_pager_key"
const val enableTransitionsWebtoon = "pref_enable_transitions_webtoon_key"
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed" const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
const val showPageNumber = "pref_show_page_number_key" const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplit = "pref_dual_page_split" const val dualPageSplitPaged = "pref_dual_page_split"
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
const val dualPageInvertPaged = "pref_dual_page_invert"
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
const val showReadingMode = "pref_show_reading_mode" const val showReadingMode = "pref_show_reading_mode"
@@ -73,6 +81,10 @@ object PreferenceKeys {
const val navigationModeWebtoon = "reader_navigation_mode_webtoon" const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
const val webtoonSidePadding = "webtoon_side_padding" const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key" const val portraitColumns = "pref_library_columns_portrait_key"
@@ -114,6 +126,7 @@ object PreferenceKeys {
const val libraryUpdateRestriction = "library_update_restriction" const val libraryUpdateRestriction = "library_update_restriction"
const val libraryUpdateCategories = "library_update_categories" const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization" const val libraryUpdatePrioritization = "library_update_prioritization"
@@ -158,6 +171,7 @@ object PreferenceKeys {
const val downloadNew = "download_new" const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories" const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val libraryDisplayMode = "pref_display_mode_library" const val libraryDisplayMode = "pref_display_mode_library"
@@ -183,7 +197,7 @@ object PreferenceKeys {
const val searchPinnedSourcesOnly = "search_pinned_sources_only" const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val enableDoh = "enable_doh" const val dohProvider = "doh_provider"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read" const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
@@ -199,6 +213,8 @@ object PreferenceKeys {
const val incognitoMode = "incognito_mode" const val incognitoMode = "incognito_mode"
const val createLegacyBackup = "create_legacy_backup"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
@@ -313,6 +329,8 @@ object PreferenceKeys {
const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key" const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
const val mangadexSyncToLibraryIndexes = "pref_mangadex_sync_to_library_indexes"
const val preferredMangaDexId = "preferred_mangaDex_id" const val preferredMangaDexId = "preferred_mangaDex_id"
const val dataSaver = "data_saver" const val dataSaver = "data_saver"
@@ -339,11 +357,15 @@ object PreferenceKeys {
const val sortTagsForLibrary = "sort_tags_for_library" const val sortTagsForLibrary = "sort_tags_for_library"
const val createLegacyBackup = "create_legacy_backup"
const val dontDeleteFromCategories = "dont_delete_from_categories" const val dontDeleteFromCategories = "dont_delete_from_categories"
const val extensionRepos = "extension_repos" const val extensionRepos = "extension_repos"
const val cropBordersContinuesVertical = "crop_borders_continues_vertical" const val cropBordersContinuousVertical = "crop_borders_continues_vertical"
const val landscapeVerticalSeekbar = "pref_show_vert_seekbar_landscape"
const val leftVerticalSeekbar = "pref_left_handed_vertical_seekbar"
const val forceHorizontalSeekbar = "pref_force_horz_seekbar"
} }
@@ -5,6 +5,8 @@ package eu.kanade.tachiyomi.data.preference
*/ */
object PreferenceValues { object PreferenceValues {
/* ktlint-disable experimental:enum-entry-name-case */
// Keys are lowercase to match legacy string values // Keys are lowercase to match legacy string values
enum class ThemeMode { enum class ThemeMode {
light, light,
@@ -25,8 +27,11 @@ object PreferenceValues {
amoled, amoled,
red, red,
midnightdusk, midnightdusk,
hotpink,
} }
/* ktlint-enable experimental:enum-entry-name-case */
enum class DisplayMode { enum class DisplayMode {
COMPACT_GRID, COMPACT_GRID,
COMFORTABLE_GRID, COMFORTABLE_GRID,
@@ -22,7 +22,7 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> { fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
block(get()) block(get())
return asFlow() return asFlow()
.onEach { block(it) } .onEach { block(it) }
@@ -36,6 +36,10 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item) set(get() - item)
} }
fun Preference<Boolean>.toggle() {
set(!get())
}
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@@ -83,13 +87,21 @@ class PreferencesHelper(val context: Context) {
fun rotation() = flowPrefs.getInt(Keys.rotation, 1) fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true) fun pageTransitionsPager() = flowPrefs.getBoolean(Keys.enableTransitionsPager, true)
fun pageTransitionsWebtoon() = flowPrefs.getBoolean(Keys.enableTransitionsWebtoon, true)
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500) fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true) fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false) fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true) fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
@@ -143,6 +155,10 @@ class PreferencesHelper(val context: Context) {
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0) fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0) fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0) fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
@@ -204,6 +220,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi")) fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet()) fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
@@ -254,6 +271,7 @@ class PreferencesHelper(val context: Context) {
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false) fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet()) fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun lang() = prefs.getString(Keys.lang, "") fun lang() = prefs.getString(Keys.lang, "")
@@ -267,7 +285,7 @@ class PreferencesHelper(val context: Context) {
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false) fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "") fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
@@ -425,6 +443,8 @@ class PreferencesHelper(val context: Context) {
fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true) fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
fun mangadexSyncToLibraryIndexes() = flowPrefs.getStringSet(Keys.mangadexSyncToLibraryIndexes, emptySet())
fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2) fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false) fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
@@ -455,5 +475,11 @@ class PreferencesHelper(val context: Context) {
fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet()) fun extensionRepos() = flowPrefs.getStringSet(Keys.extensionRepos, emptySet())
fun cropBordersContinuesVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuesVertical, false) fun cropBordersContinuousVertical() = flowPrefs.getBoolean(Keys.cropBordersContinuousVertical, false)
fun forceHorizontalSeekbar() = flowPrefs.getBoolean(Keys.forceHorizontalSeekbar, false)
fun landscapeVerticalSeekbar() = flowPrefs.getBoolean(Keys.landscapeVerticalSeekbar, false)
fun leftVerticalSeekbar() = flowPrefs.getBoolean(Keys.leftVerticalSeekbar, false)
} }
@@ -35,6 +35,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
private val api by lazy { AnilistApi(client, interceptor) } private val api by lazy { AnilistApi(client, interceptor) }
override val supportsReadingDates: Boolean = true
private val scorePreference = preferences.anilistScoreType() private val scorePreference = preferences.anilistScoreType()
init { init {
@@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.afollestad.date.dayOfMonth
import com.afollestad.date.month
import com.afollestad.date.year
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.POST import eu.kanade.tachiyomi.network.POST
@@ -9,6 +12,7 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
@@ -30,8 +34,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = val query = """
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id | id
@@ -65,10 +68,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = val query = """
""" |mutation UpdateManga(
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) { |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) { |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) {
|id |id
|status |status
|progress |progress
@@ -82,6 +90,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("progress", track.last_chapter_read) put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus()) put("status", track.toAnilistStatus())
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date))
} }
} }
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
@@ -92,8 +102,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun search(search: String): List<TrackSearch> { suspend fun search(search: String): List<TrackSearch> {
return withIOContext { return withIOContext {
val query = val query = """
"""
|query Search(${'$'}query: String) { |query Search(${'$'}query: String) {
|Page (perPage: 50) { |Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) { |media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@@ -143,8 +152,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun findLibManga(track: Track, userid: Int): Track? { suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext { return withIOContext {
val query = val query = """
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) { |query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page { |Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) { |mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@@ -152,6 +160,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status |status
|scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
|progress |progress
|startedAt {
|year
|month
|day
|}
|completedAt {
|year
|month
|day
|}
|media { |media {
|id |id
|title { |title {
@@ -209,8 +227,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun getCurrentUser(): Pair<Int, String> { suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext { return withIOContext {
val query = val query = """
"""
|query User { |query User {
|Viewer { |Viewer {
|id |id
@@ -243,21 +260,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
private fun jsonToALManga(struct: JsonObject): ALManga { private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
(
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
?: 0
) - 1,
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga( return ALManga(
struct["id"]!!.jsonPrimitive.int, struct["id"]!!.jsonPrimitive.int,
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content, struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
@@ -265,7 +267,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["description"]!!.jsonPrimitive.contentOrNull, struct["description"]!!.jsonPrimitive.contentOrNull,
struct["type"]!!.jsonPrimitive.content, struct["type"]!!.jsonPrimitive.content,
struct["status"]!!.jsonPrimitive.contentOrNull ?: "", struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
date, parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0 struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
) )
} }
@@ -276,10 +278,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["status"]!!.jsonPrimitive.content, struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int, struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int, struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject) jsonToALManga(struct["media"]!!.jsonObject)
) )
} }
private fun parseDate(struct: JsonObject, dateKey: String): Long {
return try {
val date = Calendar.getInstance()
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
)
date.timeInMillis
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
return buildJsonObject {
put("year", JsonNull)
put("month", JsonNull)
put("day", JsonNull)
}
}
val calendar = Calendar.getInstance()
calendar.timeInMillis = dateValue
return buildJsonObject {
put("year", calendar.year)
put("month", calendar.month + 1)
put("day", calendar.dayOfMonth)
}
}
companion object { companion object {
private const val clientId = "385" private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/" private const val apiUrl = "https://graphql.anilist.co/"
@@ -44,6 +44,8 @@ data class ALUserManga(
val list_status: String, val list_status: String,
val score_raw: Int, val score_raw: Int,
val chapters_read: Int, val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga val manga: ALManga
) { ) {
@@ -51,6 +53,8 @@ data class ALUserManga(
media_id = manga.media_id media_id = manga.media_id
status = toTrackStatus() status = toTrackStatus()
score = score_raw.toFloat() score = score_raw.toFloat()
started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read last_chapter_read = chapters_read
library_id = this@ALUserManga.library_id library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters total_chapters = manga.total_chapters
@@ -45,8 +45,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return if (remoteTrack != null && statusTrack != null) { return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
track.status = remoteTrack.status track.status = statusTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track) refresh(track)
} else { } else {
// Set default fields if it's not found in the list // Set default fields if it's not found in the list
@@ -66,7 +68,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
track.copyPersonalFrom(remoteStatusTrack!!) track.copyPersonalFrom(remoteStatusTrack!!)
api.findLibManga(track)?.let { remoteTrack -> api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
} }
return track return track
} }
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return withIOContext { return withIOContext {
// read status update // read status update
val sbody = FormBody.Builder() val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus()) .add("status", track.toBangumiStatus())
.build() .build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody)) authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
@@ -91,12 +93,24 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
private fun jsonToSearch(obj: JsonObject): TrackSearch { private fun jsonToSearch(obj: JsonObject): TrackSearch {
val coverUrl = if (obj["images"] is JsonObject) {
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
} else {
// Sometimes JsonNull
""
}
val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int
} else {
0
}
return TrackSearch.create(TrackManager.BANGUMI).apply { return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"]!!.jsonPrimitive.int media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name_cn"]!!.jsonPrimitive.content title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content cover_url = coverUrl
summary = obj["name"]!!.jsonPrimitive.content summary = obj["name"]!!.jsonPrimitive.content
tracking_url = obj["url"]!!.jsonPrimitive.content tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
} }
} }
@@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.build() .build()
// TODO: get user readed chapter here // TODO: get user readed chapter here
authClient.newCall(requestUserRead) var response = authClient.newCall(requestUserRead).await()
.await() var responseBody = response.body?.string().orEmpty()
.parseAs<Collection>() if (responseBody.isEmpty()) {
.let { throw Exception("Null Response")
}
if (responseBody.contains("\"code\":400")) {
null
} else {
json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!! track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!! track.last_chapter_read = it.ep_status!!
track.score = it.rating!!
track track
} }
}
} }
} }
@@ -8,7 +8,7 @@ data class Collection(
val comment: String? = "", val comment: String? = "",
val ep_status: Int? = 0, val ep_status: Int? = 0,
val lasttouch: Int? = 0, val lasttouch: Int? = 0,
val rating: Int? = 0, val rating: Float? = 0f,
val status: Status? = Status(), val status: Status? = Status(),
val tag: List<String?>? = listOf(), val tag: List<String?>? = listOf(),
val user: User? = User(), val user: User? = User(),
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@@ -46,80 +47,84 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track = update(track) override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
val mdex = mdex ?: throw MangaDexNotFoundException() return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url) val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
val followStatus = FollowStatus.fromInt(track.status) val followStatus = FollowStatus.fromInt(track.status)
// this updates the follow status in the metadata // this updates the follow status in the metadata
// allow follow status to update // allow follow status to update
if (remoteTrack.status != followStatus.int) { if (remoteTrack.status != followStatus.int) {
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus) mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int remoteTrack.status = followStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
}
if (track.score.toInt() > 0) {
mdex.updateRating(track)
}
// 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
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
} }
mdex.updateReadingProgress(track) if (track.score.toInt() > 0) {
} else if (track.last_chapter_read != 0) { mdex.updateRating(track)
// When followStatus has been changed to unfollowed 0 out read chapters since dex does }
track.last_chapter_read = 0
// 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
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
}
mdex.updateReadingProgress(track)
} 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
} }
return track
} }
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
override suspend fun bind(track: Track): Track = update(refresh(track)) override suspend fun bind(track: Track): Track = update(refresh(track).also { if (it.status == FollowStatus.UNFOLLOWED.int) it.status = FollowStatus.READING.int })
override suspend fun refresh(track: Track): Track { override suspend fun refresh(track: Track): Track {
val mdex = mdex ?: throw MangaDexNotFoundException() return withIOContext {
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) val mdex = mdex ?: throw MangaDexNotFoundException()
track.copyPersonalFrom(remoteTrack) val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { track.copyPersonalFrom(remoteTrack)
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0 if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
}
track
} }
return track
} }
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track { fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
val track = Track.create(TrackManager.MDLIST) return Track.create(TrackManager.MDLIST).apply {
track.manga_id = dbManga.id!! manga_id = dbManga.id!!
track.status = FollowStatus.UNFOLLOWED.int status = FollowStatus.UNFOLLOWED.int
track.tracking_url = MdUtil.baseUrl + mdManga.url tracking_url = MdUtil.baseUrl + mdManga.url
track.title = mdManga.title title = mdManga.title
return track }
} }
override suspend fun search(query: String): List<TrackSearch> { override suspend fun search(query: String): List<TrackSearch> {
val mdex = mdex ?: throw MangaDexNotFoundException() return withIOContext {
return mdex.fetchSearchManga(0, query, mdex.getFilterList()) val mdex = mdex ?: throw MangaDexNotFoundException()
.flatMap { page -> mdex.fetchSearchManga(0, query, mdex.getFilterList())
runAsObservable({ .flatMap { page ->
page.mangas.map { runAsObservable({
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo())) page.mangas.map {
} toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
}) }
} })
.awaitSingle() }
.awaitSingle()
}
} }
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply { private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
@@ -131,5 +136,8 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun login(username: String, password: String): Unit = throw Exception("not used") override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
override val isLogged: Boolean
get() = false
class MangaDexNotFoundException : Exception("Mangadex not enabled") class MangaDexNotFoundException : Exception("Mangadex not enabled")
} }
@@ -32,8 +32,8 @@ class GithubUpdateChecker {
.parseAs<GithubRelease>() .parseAs<GithubRelease>()
.let { .let {
// Check if latest version is different from current version // Check if latest version is different from current version
if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) { if (/* SY --> */ isNewVersionSY(it.version) /* SY <-- */) {
GithubUpdateResult.NewUpdate(it) GithubUpdateResult.NewUpdate(it)
} else { } else {
GithubUpdateResult.NoNewUpdate() GithubUpdateResult.NoNewUpdate()
} }
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.elvishew.xlog.XLog
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.log.xLogD
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
@@ -156,15 +156,15 @@ class ExtensionManager(
// EXH --> // EXH -->
private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> { private fun <T : Extension> Iterable<T>.filterNotBlacklisted(): List<T> {
val blacklistEnabled = preferences.enableSourceBlacklist().get() val blacklistEnabled = preferences.enableSourceBlacklist().get()
return filter { return filterNot { extension ->
if (it.isBlacklisted(blacklistEnabled)) { extension.isBlacklisted(blacklistEnabled)
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: %s, pkgName: %s)!", it.name, it.pkgName) .also {
false if (it) this@ExtensionManager.xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
} else true }
} }
} }
fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean { private fun Extension.isBlacklisted(blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get()): Boolean {
return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled return pkgName in BlacklistedSources.BLACKLISTED_EXTENSIONS && blacklistEnabled
} }
// EXH <-- // EXH <--
@@ -333,7 +333,7 @@ class ExtensionManager(
private fun registerNewExtension(extension: Extension.Installed) { private fun registerNewExtension(extension: Extension.Installed) {
// SY --> // SY -->
if (extension.isBlacklisted()) { if (extension.isBlacklisted()) {
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName) xLogD("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName)
return return
} }
// SY <-- // SY <--
@@ -351,7 +351,7 @@ class ExtensionManager(
private fun registerUpdatedExtension(extension: Extension.Installed) { private fun registerUpdatedExtension(extension: Extension.Installed) {
// SY --> // SY -->
if (extension.isBlacklisted()) { if (extension.isBlacklisted()) {
XLog.tag("ExtensionManager").d("Removing blacklisted extension: (name: String, pkgName: %s)!", extension.name, extension.pkgName) xLogD("Removing blacklisted extension: (name: %s, pkgName: %s)!", extension.name, extension.pkgName)
return return
} }
// SY <-- // SY <--
@@ -32,7 +32,7 @@ internal class ExtensionGithubApi {
.let { parseResponse(it) } .let { parseResponse(it) }
} /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath -> } /* SY --> */ + preferences.extensionRepos().get().flatMap { repoPath ->
val url = "$BASE_URL$repoPath/repo/" val url = "$BASE_URL$repoPath/repo/"
networkService.client networkService.client
.newCall(GET("${url}index.min.json")) .newCall(GET("${url}index.min.json"))
.await() .await()
.parseAs<JsonArray>() .parseAs<JsonArray>()
@@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.") Timber.w(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
@@ -171,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
companion object { companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare") private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance") private val COOKIE_NAMES = listOf("cf_clearance")
} }
} }
@@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
*/
const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
.build()
)
fun OkHttpClient.Builder.dohGoogle() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("8.8.4.4"),
InetAddress.getByName("8.8.8.8")
)
.build()
)
@@ -4,13 +4,10 @@ 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 okhttp3.Cache import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
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.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) { /* SY --> */ open /* SY <-- */ class NetworkHelper(context: Context) {
@@ -38,25 +35,9 @@ import java.util.concurrent.TimeUnit
builder.addInterceptor(httpLoggingInterceptor) builder.addInterceptor(httpLoggingInterceptor)
} }
if (preferences.enableDoh()) { when (preferences.dohProvider()) {
builder.dns( PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
DnsOverHttps.Builder().client(builder.build()) PREF_DOH_GOOGLE -> builder.dohGoogle()
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
)
.build()
)
} }
builder.build() builder.build()
@@ -33,7 +33,7 @@ import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(private val context: Context) : CatalogueSource {
companion object { companion object {
const val ID = 0L const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/" const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg" private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub") private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
@@ -64,6 +64,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
val c = context.getString(R.string.app_name) + File.separator + "local" val c = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) } return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
} }
// SY -->
val json = Json {
prettyPrint = true
}
// SY <--
} }
override val id = ID override val id = ID
@@ -151,19 +157,16 @@ class LocalSource(private val context: Context) : CatalogueSource {
// SY --> // SY -->
fun updateMangaInfo(manga: SManga) { fun updateMangaInfo(manga: SManga) {
val directory = getBaseDirectories(context).mapNotNull { File(it, manga.url) }.find { val directory = getBaseDirectories(context).map { File(it, manga.url) }.find {
it.exists() it.exists()
} ?: return } ?: return
val json = Json {
prettyPrint = true
}
val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name val existingFileName = directory.listFiles()?.find { it.extension == "json" }?.name
val file = File(directory, existingFileName ?: "info.json") val file = File(directory, existingFileName ?: "info.json")
file.writeText(json.encodeToString(manga.toJson())) file.writeText(json.encodeToString(manga.toJson()))
} }
private fun SManga.toJson(): MangaJson { private fun SManga.toJson(): MangaJson {
return MangaJson(title, author, artist, description, genre?.split(", ")?.toTypedArray()) return MangaJson(title, author, artist, description, genre?.split(", "), status)
} }
@Serializable @Serializable
@@ -172,7 +175,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
val author: String?, val author: String?,
val artist: String?, val artist: String?,
val description: String?, val description: String?,
val genre: Array<String>? val genre: List<String>?,
val status: Int
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -10,15 +9,14 @@ 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
import eu.kanade.tachiyomi.source.online.english.EightMuses import eu.kanade.tachiyomi.source.online.english.EightMuses
import eu.kanade.tachiyomi.source.online.english.HBrowse import eu.kanade.tachiyomi.source.online.english.HBrowse
import eu.kanade.tachiyomi.source.online.english.HentaiCafe
import eu.kanade.tachiyomi.source.online.english.Pururin import eu.kanade.tachiyomi.source.online.english.Pururin
import eu.kanade.tachiyomi.source.online.english.Tsumino import eu.kanade.tachiyomi.source.online.english.Tsumino
import exh.log.xLogD
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
@@ -26,7 +24,6 @@ import exh.source.EIGHTMUSES_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource
import exh.source.HBROWSE_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID
import exh.source.HENTAI_CAFE_SOURCE_ID
import exh.source.PERV_EDEN_EN_SOURCE_ID import exh.source.PERV_EDEN_EN_SOURCE_ID
import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.PERV_EDEN_IT_SOURCE_ID
import exh.source.PURURIN_SOURCE_ID import exh.source.PURURIN_SOURCE_ID
@@ -115,7 +112,7 @@ open class SourceManager(private val context: Context) {
} else DELEGATED_SOURCES[sourceQName] } else DELEGATED_SOURCES[sourceQName]
} else null } else null
val newSource = if (source is HttpSource && delegate != null) { val newSource = if (source is HttpSource && delegate != null) {
XLog.tag("SourceManager").d("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName) xLogD("Delegating source: %s -> %s!", sourceQName, delegate.newSourceClass.qualifiedName)
val enhancedSource = EnhancedHttpSource( val enhancedSource = EnhancedHttpSource(
source, source,
delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context) delegate.newSourceClass.constructors.find { it.parameters.size == 2 }!!.call(source, context)
@@ -132,7 +129,7 @@ open class SourceManager(private val context: Context) {
} else source } else source
if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) { if (source.id in BlacklistedSources.BLACKLISTED_EXT_SOURCES) {
XLog.tag("SourceManager").d("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang) xLogD("Removing blacklisted source: (id: %s, name: %s, lang: %s)!", source.id, source.name, (source as? CatalogueSource)?.lang)
return return
} }
// EXH <-- // EXH <--
@@ -155,13 +152,12 @@ open class SourceManager(private val context: Context) {
// SY --> // SY -->
private fun createEHSources(): List<Source> { private fun createEHSources(): List<Source> {
val exSrcs = mutableListOf<HttpSource>( val sources = listOf<HttpSource>(
EHentai(EH_SOURCE_ID, false, context) EHentai(EH_SOURCE_ID, false, context)
) )
if (prefs.enableExhentai().get()) { return if (prefs.enableExhentai().get()) {
exSrcs += EHentai(EXH_SOURCE_ID, true, context) sources + EHentai(EXH_SOURCE_ID, true, context)
} } else sources
return exSrcs
} }
// SY <-- // SY <--
@@ -195,12 +191,6 @@ open class SourceManager(private val context: Context) {
companion object { companion object {
private const val fillInSourceId = Long.MAX_VALUE private const val fillInSourceId = Long.MAX_VALUE
val DELEGATED_SOURCES = listOf( val DELEGATED_SOURCES = listOf(
DelegatedSource(
"Hentai Cafe",
HENTAI_CAFE_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.foolslide.HentaiCafe",
HentaiCafe::class
),
DelegatedSource( DelegatedSource(
"Pururin", "Pururin",
PURURIN_SOURCE_ID, PURURIN_SOURCE_ID,
@@ -213,13 +203,13 @@ open class SourceManager(private val context: Context) {
"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,
@@ -229,7 +219,7 @@ open class SourceManager(private val context: Context) {
DelegatedSource( DelegatedSource(
"8Muses", "8Muses",
EIGHTMUSES_SOURCE_ID, EIGHTMUSES_SOURCE_ID,
"eu.kanade.tachiyomi.extension.all.eromuse.EroMuse", "eu.kanade.tachiyomi.extension.en.eightmuses.EightMuses",
EightMuses::class EightMuses::class
), ),
DelegatedSource( DelegatedSource(
@@ -276,7 +266,7 @@ open class SourceManager(private val context: Context) {
get() = internalMap.size get() = internalMap.size
override fun containsKey(key: K): Boolean = internalMap.containsKey(key) override fun containsKey(key: K): Boolean = internalMap.containsKey(key)
override fun containsValue(value: V): Boolean = internalMap.containsValue(value) override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
override fun get(key: K): V? = get(key) override fun get(key: K): V? = internalMap[key]
override fun isEmpty(): Boolean = internalMap.isEmpty() override fun isEmpty(): Boolean = internalMap.isEmpty()
override val entries: MutableSet<MutableMap.MutableEntry<K, V>> override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = internalMap.entries get() = internalMap.entries
@@ -2,8 +2,26 @@ package eu.kanade.tachiyomi.source.model
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
/* SY --> */ open /* SY <-- */ class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean) /* SY --> */ open /* SY <-- */ class MangasPage(open val mangas: List<SManga>, open val hasNextPage: Boolean) {
// SY -->
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MangasPage) return false
if (mangas != other.mangas) return false
if (hasNextPage != other.hasNextPage) return false
return true
}
override fun hashCode(): Int {
var result = mangas.hashCode()
result = 31 * result + hasNextPage.hashCode()
return result
}
// SY <--
}
// SY --> // SY -->
class MetadataMangasPage(mangas: List<SManga>, hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage) data class MetadataMangasPage(override val mangas: List<SManga>, override val hasNextPage: Boolean, val mangasMetadata: List<RaisedSearchMetadata>) : MangasPage(mangas, hasNextPage)
// SY <-- // SY <--
@@ -35,6 +35,8 @@ interface SManga : Serializable {
get() = (this as? MangaImpl)?.ogDesc ?: description get() = (this as? MangaImpl)?.ogDesc ?: description
val originalGenre: String? val originalGenre: String?
get() = (this as? MangaImpl)?.ogGenre ?: genre get() = (this as? MangaImpl)?.ogGenre ?: genre
val originalStatus: Int
get() = (this as? MangaImpl)?.ogStatus ?: status
// SY <-- // SY <--
fun copyFrom(other: SManga) { fun copyFrom(other: SManga) {
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadataCompletable
import exh.util.executeOnIO import exh.util.executeOnIO
import rx.Completable import rx.Completable
import rx.Single import rx.Single
@@ -72,7 +72,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
}.flatMapCompletable { }.flatMapCompletable {
if (mangaId != null) { if (mangaId != null) {
it.mangaId = mangaId it.mangaId = mangaId
db.insertFlatMetadata(it.flatten()) db.insertFlatMetadataCompletable(it.flatten())
} else Completable.complete() } else Completable.complete()
} }
} }
@@ -87,7 +87,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
parseInfoIntoMetadata(metadata, input) parseInfoIntoMetadata(metadata, input)
if (mangaId != null) { if (mangaId != null) {
metadata.mangaId = mangaId metadata.mangaId = mangaId
db.insertFlatMetadataAsync(metadata.flatten()).await() db.insertFlatMetadata(metadata.flatten())
} }
return metadata.createMangaInfo(manga) return metadata.createMangaInfo(manga)
@@ -119,7 +119,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
val newMetaSingle = Single.just(newMeta) val newMetaSingle = Single.just(newMeta)
if (mangaId != null) { if (mangaId != null) {
newMeta.mangaId = mangaId newMeta.mangaId = mangaId
db.insertFlatMetadata(newMeta.flatten()).andThen(newMetaSingle) db.insertFlatMetadataCompletable(newMeta.flatten()).andThen(newMetaSingle)
} else newMetaSingle } else newMetaSingle
} }
} else Single.just(existingMeta) } else Single.just(existingMeta)
@@ -146,7 +146,7 @@ interface MetadataSource<M : RaisedSearchMetadata, I> : CatalogueSource {
parseInfoIntoMetadata(newMeta, input) parseInfoIntoMetadata(newMeta, input)
if (mangaId != null) { if (mangaId != null) {
newMeta.mangaId = mangaId newMeta.mangaId = mangaId
db.insertFlatMetadataAsync(newMeta.flatten()).await().let { newMeta } db.insertFlatMetadata(newMeta.flatten()).let { newMeta }
} else newMeta } else newMeta
} }
} }
@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.source.online.all
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.annotations.Nsfw import eu.kanade.tachiyomi.annotations.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
@@ -31,6 +30,7 @@ import exh.eh.EHTags
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.eh.EHentaiUpdateWorkerConstants import exh.eh.EHentaiUpdateWorkerConstants
import exh.eh.GalleryEntry import exh.eh.GalleryEntry
import exh.log.xLogD
import exh.metadata.MetadataUtil import exh.metadata.MetadataUtil
import exh.metadata.metadata.EHentaiSearchMetadata import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
@@ -40,7 +40,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.toGenreString
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.ui.login.LoginController import exh.ui.login.EhLoginActivity
import exh.ui.metadata.adapters.EHentaiDescriptionAdapter import exh.ui.metadata.adapters.EHentaiDescriptionAdapter
import exh.util.UriFilter import exh.util.UriFilter
import exh.util.UriGroup import exh.util.UriGroup
@@ -229,7 +229,7 @@ class EHentai(
} else { } else {
parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean() parsedLocation.queryParameter(REVERSE_PARAM)!!.toBoolean()
} }
Pair(parsedMangas, hasNextPage) parsedMangas to hasNextPage
} }
private fun getGenre(element: Element? = null, genreString: String? = null): String? { private fun getGenre(element: Element? = null, genreString: String? = null): String? {
@@ -325,7 +325,7 @@ class EHentai(
url = EHentaiSearchMetadata.normalizeUrl(parentLink) url = EHentaiSearchMetadata.normalizeUrl(parentLink)
} else break } else break
} else { } else {
XLog.tag("EHentai").d("Parent cache hit: %s!", gid) this@EHentai.xLogD("Parent cache hit: %s!", gid)
url = EHentaiSearchMetadata.idAndTokenToUrl( url = EHentaiSearchMetadata.idAndTokenToUrl(
cachedParent.gId, cachedParent.gId,
cachedParent.gToken cachedParent.gToken
@@ -613,7 +613,7 @@ class EHentai(
lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME lastUpdateCheck - datePosted!! > EHentaiUpdateWorkerConstants.GALLERY_AGE_TIME
) { ) {
aged = true aged = true
XLog.tag("EHentai").d("aged %s - too old", title) this@EHentai.xLogD("aged %s - too old", title)
} }
// Parse ratings // Parse ratings
@@ -713,7 +713,7 @@ class EHentai(
page++ page++
} while (parsed.second) } while (parsed.second)
return Pair(result.toList(), favNames!!) return Pair(result.toList(), favNames.orEmpty())
} }
fun spPref() = if (exh) { fun spPref() = if (exh) {
@@ -725,9 +725,9 @@ class EHentai(
private 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 (preferences.enableExhentai().get()) { if (preferences.enableExhentai().get()) {
cookies[LoginController.MEMBER_ID_COOKIE] = preferences.memberIdVal().get() cookies[EhLoginActivity.MEMBER_ID_COOKIE] = preferences.memberIdVal().get()
cookies[LoginController.PASS_HASH_COOKIE] = preferences.passHashVal().get() cookies[EhLoginActivity.PASS_HASH_COOKIE] = preferences.passHashVal().get()
cookies[LoginController.IGNEOUS_COOKIE] = preferences.igneousVal().get() cookies[EhLoginActivity.IGNEOUS_COOKIE] = preferences.igneousVal().get()
cookies["sp"] = sp.toString() cookies["sp"] = sp.toString()
val sessionKey = preferences.exhSettingsKey().get() val sessionKey = preferences.exhSettingsKey().get()
@@ -879,13 +879,20 @@ class EHentai(
stringBuilder.append(" ") stringBuilder.append(" ")
} }
XLog.tag("EHentai").d(stringBuilder.toString()) return stringBuilder.toString().trim().also { xLogD(it) }
return stringBuilder.toString().trim()
} }
data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean) data class AdvSearchEntry(val search: Pair<String, String>, val exclude: Boolean)
class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) : Filter.AutoComplete(name = "Tags", hint = "Search tags here (limit of 8)", values = tags, skipAutoFillTags = skipAutoFillTags, excludePrefix = excludePrefix, state = emptyList()) class AutoCompleteTags(tags: List<String>, skipAutoFillTags: List<String>, excludePrefix: String) :
Filter.AutoComplete(
name = "Tags",
hint = "Search tags here (limit of 8)",
values = tags,
skipAutoFillTags = skipAutoFillTags,
excludePrefix = excludePrefix,
state = emptyList()
)
class MinPagesOption : PageOption("Minimum Pages", "f_spf") class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt") class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import androidx.core.text.HtmlCompat
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -48,12 +47,18 @@ import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.EOFException
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@@ -73,11 +78,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
RandomMangaSource { RandomMangaSource {
override val lang: String = delegate.lang override val lang: String = delegate.lang
override val headers: Headers override val headers: Headers = super.headers.newBuilder().apply {
get() = super.headers.newBuilder().apply { add("X-Requested-With", "XMLHttpRequest")
add("X-Requested-With", "XMLHttpRequest") add("Referer", MdUtil.baseUrl)
add("Referer", MdUtil.baseUrl) }.build()
}.build()
private val mdLang by lazy { private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang MdLang.values().find { it.lang == lang }?.dexLang ?: lang
@@ -198,26 +202,26 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password) add("login_password", password)
add("no_js", "1") add("no_js", "1")
add("remember_me", "1") add("remember_me", "1")
add("two_factor", twoFactorCode)
} }
twoFactorCode.let { runCatching {
formBody.add("two_factor", it) client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await().closeQuietly()
} }
val response = client.newCall( val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await()
withIOContext { response.body?.string() }.let { result -> withIOContext { response.body?.string() }.let { jsonData ->
if (result != null && result.isEmpty()) { if (jsonData != null) {
true MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
} else { } else {
val error = result?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() } throw Exception("Json data was null")
throw Exception(error)
} }
} }
} }
@@ -233,11 +237,17 @@ class MangaDex(delegate: HttpSource, val context: Context) :
if (token.isNullOrEmpty()) { if (token.isNullOrEmpty()) {
return@withIOContext true return@withIOContext true
} }
val result = client.newCall( try {
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build() val result = client.newCall(
).await() POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
val resultStr = withIOContext { result.body?.string() } ).await()
if (resultStr?.contains("success", true) == true) { val resultStr = withIOContext { result.body?.string() }
if (resultStr?.contains("success", true) == true) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
} catch (e: EOFException) {
network.cookieManager.remove(httpUrl) network.cookieManager.remove(httpUrl)
trackManager.mdList.logout() trackManager.mdList.logout()
return@withIOContext true return@withIOContext true
@@ -269,7 +279,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> { suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
return MangaHandler(client, headers, lang).getTrackingInfo(track, useLowQualityThumbnail()) return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
} }
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
@@ -281,7 +291,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override suspend fun fetchRandomMangaUrl(): String { override suspend fun fetchRandomMangaUrl(): String {
return MangaHandler(client, headers, mdLang).fetchRandomMangaId() return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
} }
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> { fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.executeOnIO import exh.util.executeOnIO
@@ -181,11 +181,11 @@ class MergedSource : HttpSource() {
} }
manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga()) manga.copyFrom(source.getMangaDetails(manga.toMangaInfo()).toSManga())
try { try {
manga.id = db.insertManga(manga).executeOnIO().insertedId() manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
mangaId = manga.id mangaId = manga.id
db.insertNewMergedMangaId(this).executeOnIO() db.insertNewMergedMangaId(this).executeAsBlocking()
} catch (e: Exception) { } catch (e: Exception) {
XLog.tag("MergedSource").enableStackTrace(e.stackTrace.contentToString(), 5) xLogW("Error inserting merged manga id", e)
} }
} }
return LoadedMangaSource(source, manga, this) return LoadedMangaSource(source, manga, this)
@@ -1,143 +0,0 @@
package eu.kanade.tachiyomi.source.online.english
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.metadata.metadata.HentaiCafeSearchMetadata
import exh.metadata.metadata.HentaiCafeSearchMetadata.Companion.TAG_TYPE_DEFAULT
import exh.metadata.metadata.base.RaisedSearchMetadata.Companion.TAG_TYPE_VIRTUAL
import exh.metadata.metadata.base.RaisedTag
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.HentaiCafeDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.nodes.Document
import rx.Observable
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
class HentaiCafe(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
MetadataSource<HentaiCafeSearchMetadata, Document>,
UrlImportableSource {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang = "en"
/**
* The class of the metadata used by this source
*/
override val metaClass = HentaiCafeSearchMetadata::class
// Support direct URL importing
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.flatMap {
parseToManga(manga, it.asJsoup()).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
}
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
return parseToManga(manga, response.asJsoup())
}
/**
* Parse the supplied input into the supplied metadata object
*/
override fun parseIntoMetadata(metadata: HentaiCafeSearchMetadata, input: Document) {
with(metadata) {
url = input.location()
title = input.select("h3").text()
val contentElement = input.select(".entry-content").first()
thumbnailUrl = contentElement.child(0).child(0).attr("src")
fun filterableTagsOfType(type: String) = contentElement.select("a")
.filter { "$baseUrl/hc.fyi/$type/" in it.attr("href") }
.map { it.text() }
tags.clear()
tags += filterableTagsOfType("tag").map {
RaisedTag(null, it, TAG_TYPE_DEFAULT)
}
val artists = filterableTagsOfType("artist")
artist = artists.joinToString()
tags += artists.map {
RaisedTag("artist", it, TAG_TYPE_VIRTUAL)
}
readerId = input.select("[title=Read]").attr("href").toHttpUrlOrNull()!!.pathSegments[2]
}
}
override fun fetchChapterList(manga: SManga) = runAsObservable({
fetchOrLoadMetadata(manga.id) {
val response = client.newCall(mangaDetailsRequest(manga)).await()
response.asJsoup()
}
}).map {
listOf(
SChapter.create().apply {
url = "/manga/read/${it.readerId}/en/0/1/"
name = "Chapter"
chapter_number = 0.0f
}
)
}
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
val metadata = fetchOrLoadMetadata(manga.id()) {
val response = client.newCall(mangaDetailsRequest(manga.toSManga())).await()
response.asJsoup()
}
return listOf(
ChapterInfo(
key = "/manga/read/${metadata.readerId}/en/0/1/",
name = "Chapter",
number = 0F
)
)
}
override val matchingHosts = listOf(
"hentai.cafe"
)
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.takeUnless { it.equals("manga", true) } ?: return null
return if (lcFirstPathSegment.equals("hc.fyi", true)) {
"/$lcFirstPathSegment/${uri.pathSegments[1]}"
} else null
}
override fun getDescriptionAdapter(controller: MangaController): HentaiCafeDescriptionAdapter {
return HentaiCafeDescriptionAdapter(controller)
}
}
@@ -13,7 +13,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
private val isDarkMode: Boolean by lazy { val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get() val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) || (themeMode == Values.ThemeMode.dark) ||
( (
@@ -50,6 +50,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red Values.DarkThemeVariant.red -> R.style.Theme_Tachiyomi_Red
Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk Values.DarkThemeVariant.midnightdusk -> R.style.Theme_Tachiyomi_MidnightDusk
Values.DarkThemeVariant.hotpink -> R.style.Theme_Tachiyomi_HotPink
else -> R.style.Theme_Tachiyomi_Dark else -> R.style.Theme_Tachiyomi_Dark
} }
} }
@@ -7,11 +7,12 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler import com.bluelinelabs.conductor.changehandler.AnimatorChangeHandler
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
/** /**
* An [AnimatorChangeHandler] that will cross fade two views * An [AnimatorChangeHandler] that will remove the from view and fade in the to view
*/ */
class OneWayFadeChangeHandler : AnimatorChangeHandler { class OneWayFadeChangeHandler : FadeChangeHandler {
constructor() constructor()
constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush) constructor(removesFromViewOnPush: Boolean) : super(removesFromViewOnPush)
constructor(duration: Long) : super(duration) constructor(duration: Long) : super(duration)
@@ -33,10 +34,6 @@ class OneWayFadeChangeHandler : AnimatorChangeHandler {
return animator return animator
} }
override fun resetFromView(from: View) {
from.alpha = 1f
}
override fun copy(): ControllerChangeHandler { override fun copy(): ControllerChangeHandler {
return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush()) return OneWayFadeChangeHandler(animationDuration, removesFromViewOnPush())
} }
@@ -121,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected * [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand] * This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
*/ */
fun invalidateMenuOnExpand(): Boolean { open fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) { return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
false false
@@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* Implementation of the NucleusController that has a built-in ViewSearch
*/
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
/**
* Used to bypass the initial searchView being set to empty string after an onResume
*/
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
/**
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
*/
protected var nonSubmittedQuery: String = ""
/**
* To be called by classes that extend this subclass in onCreateOptionsMenu
*/
protected fun createOptionsMenu(
menu: Menu,
inflater: MenuInflater,
menuId: Int,
searchItemId: Int,
@StringRes queryHint: Int? = null,
restoreCurrentQuery: Boolean = true
) {
// Inflate menu
inflater.inflate(menuId, menu)
// Initialize search option.
val searchItem = menu.findItem(searchItemId)
val searchView = searchItem.actionView as SearchView
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
searchView.maxWidth = Int.MAX_VALUE
searchView.queryTextEvents()
.onEach {
val newText = it.queryText.toString()
if (newText.isNotBlank() or acceptEmptyQuery()) {
if (it is QueryTextEvent.QuerySubmitted) {
// Abstract function for implementation
// Run it first in case the old query data is needed (like BrowseSourceController)
onSearchViewQueryTextSubmit(newText)
presenter.query = newText
nonSubmittedQuery = ""
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
nonSubmittedQuery = newText
// Abstract function for implementation
onSearchViewQueryTextChange(newText)
}
}
// clear the collapsing flag
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
}
.launchIn(viewScope)
val query = presenter.query
// Restoring a query the user had not submitted
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
searchItem.expandActionView()
searchView.setQuery(nonSubmittedQuery, false)
onSearchViewQueryTextChange(nonSubmittedQuery)
} else {
if (queryHint != null) {
searchView.queryHint = applicationContext?.getString(queryHint)
}
if (restoreCurrentQuery) {
// Restoring a query the user had submitted
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
onSearchViewQueryTextChange(query)
onSearchViewQueryTextSubmit(query)
}
}
}
// Workaround for weird behavior where searchView gets empty text change despite
// query being set already, prevents the query from being cleared
binding.root.post {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
}
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setCurrentSearchViewState(SearchViewState.FOCUSED)
} else {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
}
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onSearchMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
val localSearchView = searchItem.actionView as SearchView
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
if (localSearchView.toString().isNotBlank()) {
setCurrentSearchViewState(SearchViewState.COLLAPSING)
}
onSearchMenuItemActionCollapse(item)
return true
}
}
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Until everything is up and running don't accept empty queries
setCurrentSearchViewState(SearchViewState.LOADING)
}
private fun acceptEmptyQuery(): Boolean {
return when (currentSearchViewState) {
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
else -> false
}
}
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
// When loading ignore all requests other than loaded
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
return
}
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
// COLLAPSING -> LOADED)
if ((from != null) && (currentSearchViewState != from)) {
return
}
currentSearchViewState = to
}
/**
* Called by the SearchView since since the implementation of these can vary in subclasses
* Not abstract as they are optional
*/
protected open fun onSearchViewQueryTextChange(newText: String?) {
}
protected open fun onSearchViewQueryTextSubmit(query: String?) {
}
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
}
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
}
/**
* During the conversion to SearchableNucleusController (after which I plan to merge its code
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
* triggered
*/
override fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
false
} else {
true
}
}
}
@@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope lateinit var presenterScope: CoroutineScope
/**
* Query from the view where applicable
*/
var query: String = ""
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
try { try {
super.onCreate(savedState) super.onCreate(savedState)
@@ -10,6 +10,7 @@ import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -58,6 +59,11 @@ open class ExtensionController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater) binding = ExtensionControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -104,6 +110,8 @@ open class ExtensionController :
override fun onButtonClick(position: Int) { override fun onButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
when (extension) { when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> { is Extension.Installed -> {
if (!extension.hasUpdate) { if (!extension.hasUpdate) {
openDetails(extension) openDetails(extension)
@@ -111,12 +119,6 @@ open class ExtensionController :
presenter.updateExtension(extension) presenter.updateExtension(extension)
} }
} }
is Extension.Available -> {
presenter.installExtension(extension)
}
is Extension.Untrusted -> {
openTrustDialog(extension)
}
} }
} }
@@ -147,12 +149,11 @@ open class ExtensionController :
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
if (extension is Extension.Installed) { when (extension) {
openDetails(extension) is Extension.Available -> presenter.installExtension(extension)
} else if (extension is Extension.Untrusted) { is Extension.Untrusted -> openTrustDialog(extension)
openTrustDialog(extension) is Extension.Installed -> openDetails(extension)
} }
return false return false
} }
@@ -22,6 +22,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -67,6 +68,11 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater) binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -2,11 +2,10 @@ package eu.kanade.tachiyomi.ui.browse.latest
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.asImmediateFlow import eu.kanade.tachiyomi.data.preference.asImmediateFlow
@@ -33,10 +32,6 @@ open class LatestController :
*/ */
protected var adapter: LatestAdapter? = null protected var adapter: LatestAdapter? = null
/*init {
setHasOptionsMenu(true)
}*/
/** /**
* Initiate the view with [R.layout.global_search_controller]. * Initiate the view with [R.layout.global_search_controller].
* *
@@ -46,6 +41,11 @@ open class LatestController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LatestControllerBinding.inflate(inflater) binding = LatestControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -82,34 +82,6 @@ open class LatestController :
onMangaClick(manga) onMangaClick(manga)
} }
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
/*inflater.inflate(R.menu.global_search, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
})*/
}
/** /**
* Called when the view is created * Called when the view is created
* *
@@ -30,7 +30,6 @@ import uy.kohesive.injekt.api.get
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class LatestPresenter( open class LatestPresenter(
private val sourcesToUse: List<CatalogueSource>? = null,
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get() val preferences: PreferencesHelper = Injekt.get()
@@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -47,6 +48,11 @@ class PreMigrationController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PreMigrationControllerBinding.inflate(inflater) binding = PreMigrationControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -24,7 +24,7 @@ class MigratingManga(
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
var migrationStatus: Int = MigrationStatus.RUNNING var migrationStatus = MigrationStatus.RUNNING
@Volatile @Volatile
private var manga: Manga? = null private var manga: Manga? = null
@@ -42,11 +42,3 @@ class MigratingManga(
return MigrationProcessItem(this) return MigrationProcessItem(this)
} }
} }
class MigrationStatus {
companion object {
const val RUNNING = 0
const val MANGA_FOUND = 1
const val MANGA_NOT_FOUND = 2
}
}
@@ -16,6 +16,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@@ -35,7 +36,6 @@ import eu.kanade.tachiyomi.ui.browse.migration.advanced.design.PreMigrationContr
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
@@ -48,7 +48,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import timber.log.Timber import timber.log.Timber
@@ -73,6 +75,7 @@ class MigrationListController(bundle: Bundle? = null) :
private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams) private val smartSearchEngine = SmartSearchEngine(config?.extraSearchParams)
private val migrationScope = CoroutineScope(Job() + Dispatchers.IO)
var migrationsJob: Job? = null var migrationsJob: Job? = null
private set private set
private var migratingManga: MutableList<MigratingManga>? = null private var migratingManga: MutableList<MigratingManga>? = null
@@ -83,6 +86,11 @@ class MigrationListController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationListControllerBinding.inflate(inflater) binding = MigrationListControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -99,7 +107,7 @@ class MigrationListController(bundle: Bundle? = null) :
val newMigratingManga = migratingManga ?: run { val newMigratingManga = migratingManga ?: run {
val new = config.mangaIds.map { val new = config.mangaIds.map {
MigratingManga(db, sourceManager, it, viewScope.coroutineContext + Dispatchers.IO) MigratingManga(db, sourceManager, it, migrationScope.coroutineContext)
} }
migratingManga = new.toMutableList() migratingManga = new.toMutableList()
new new
@@ -114,7 +122,7 @@ class MigrationListController(bundle: Bundle? = null) :
adapter?.updateDataSet(newMigratingManga.map { it.toModal() }) adapter?.updateDataSet(newMigratingManga.map { it.toModal() })
if (migrationsJob == null) { if (migrationsJob == null) {
migrationsJob = viewScope.launchIO { migrationsJob = migrationScope.launch {
runMigrations(newMigratingManga) runMigrations(newMigratingManga)
} }
} }
@@ -275,6 +283,7 @@ class MigrationListController(bundle: Bundle? = null) :
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
migrationScope.cancel()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
@@ -55,14 +55,12 @@ class MigrationProcessHolder(
binding.migrationMenu.setVectorCompat( binding.migrationMenu.setVectorCompat(
R.drawable.ic_more_vert_24dp, R.drawable.ic_more_vert_24dp,
view.context view.context.getResourceColor(R.attr.colorOnPrimary)
.getResourceColor(R.attr.colorOnPrimary)
) )
binding.skipManga.setVectorCompat( binding.skipManga.setVectorCompat(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
view.context.getResourceColor( view.context.getResourceColor(
R R.attr.colorOnPrimary
.attr.colorOnPrimary
) )
) )
binding.migrationMenu.isInvisible = true binding.migrationMenu.isInvisible = true
@@ -79,7 +77,8 @@ class MigrationProcessHolder(
true true
).withFadeTransaction() ).withFadeTransaction()
) )
}.launchIn(adapter.controller.viewScope) }
.launchIn(adapter.controller.viewScope)
} }
/*launchUI { /*launchUI {
@@ -115,7 +114,8 @@ class MigrationProcessHolder(
true true
).withFadeTransaction() ).withFadeTransaction()
) )
}.launchIn(adapter.controller.viewScope) }
.launchIn(adapter.controller.viewScope)
} else { } else {
binding.migrationMangaCardTo.loadingGroup.isVisible = false binding.migrationMangaCardTo.loadingGroup.isVisible = false
binding.migrationMangaCardTo.title.text = view.context.applicationContext binding.migrationMangaCardTo.title.text = view.context.applicationContext
@@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
enum class MigrationStatus {
RUNNING,
MANGA_FOUND,
MANGA_NOT_FOUND
}
@@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -53,6 +54,11 @@ class MigrationMangaController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater) binding = MigrationMangaControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -5,9 +5,12 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R 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.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
@@ -24,17 +27,37 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SearchController( class SearchController(
private var manga: Manga? = null, private var manga: Manga? = null,
private var sources: List<CatalogueSource>? = null private var sources: List<CatalogueSource>? = null
) : GlobalSearchController(manga?.originalTitle) { ) : GlobalSearchController(
manga?.originalTitle,
bundle = bundleOf(
OLD_MANGA to manga?.id,
SOURCES to sources?.map { it.id }?.toLongArray()
)
) {
private var newManga: Manga? = null private var newManga: Manga? = null
private var progress = 1 private var progress = 1
var totalProgress = 0 var totalProgress = 0
constructor(mangaId: Long, sources: LongArray) :
this(
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking(),
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>()
)
@Suppress("unused")
constructor(bundle: Bundle) : this(
bundle.getLong(OLD_MANGA),
bundle.getLongArray(SOURCES) ?: LongArray(0)
)
/** /**
* Called when controller is initialized. * Called when controller is initialized.
*/ */
@@ -58,31 +81,15 @@ class SearchController(
) )
} }
override fun onSaveInstanceState(outState: Bundle) { fun migrateManga(manga: Manga, newManga: Manga) {
outState.putSerializable(::manga.name, manga)
outState.putSerializable(::newManga.name, newManga)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
manga = savedInstanceState.getSerializable(::manga.name) as? Manga
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
}
fun migrateManga() {
val target = targetController as? MigrationInterface ?: return val target = targetController as? MigrationInterface ?: return
val manga = manga ?: return
val newManga = newManga ?: return
val nextManga = target.migrateManga(manga, newManga, true) val nextManga = target.migrateManga(manga, newManga, true)
replaceWithNewSearchController(nextManga) replaceWithNewSearchController(nextManga)
} }
fun copyManga() { fun copyManga(manga: Manga, newManga: Manga) {
val target = targetController as? MigrationInterface ?: return val target = targetController as? MigrationInterface ?: return
val manga = manga ?: return
val newManga = newManga ?: return
val nextManga = target.migrateManga(manga, newManga, false) val nextManga = target.migrateManga(manga, newManga, false)
replaceWithNewSearchController(nextManga) replaceWithNewSearchController(nextManga)
@@ -102,14 +109,15 @@ class SearchController(
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
if (targetController is MigrationListController) { if (targetController is MigrationListController) {
val migrationListController = targetController as? MigrationListController val migrationListController = targetController as? MigrationListController
val sourceManager: SourceManager by injectLazy() val sourceManager = Injekt.get<SourceManager>()
val source = sourceManager.get(manga.source) ?: return val source = sourceManager.get(manga.source) ?: return
migrationListController?.useMangaForMigration(manga, source) migrationListController?.useMangaForMigration(manga, source)
router.popCurrentController() router.popCurrentController()
return return
} }
newManga = manga newManga = manga
val dialog = MigrationDialog() val dialog =
MigrationDialog(this.manga ?: return, newManga ?: return, this)
dialog.targetController = this dialog.targetController = this
dialog.showDialog(router) dialog.showDialog(router)
} }
@@ -119,12 +127,26 @@ class SearchController(
super.onMangaClick(manga) super.onMangaClick(manga)
} }
class MigrationDialog : DialogController() { class MigrationDialog(bundle: Bundle) : DialogController(bundle) {
constructor(manga: Manga, newManga: Manga, callingController: Controller) : this(
bundleOf(
MANGA_KEY to manga,
NEW_MANGA_KEY to newManga
)
) {
this.callingController = callingController
}
private val manga: Manga = args.getSerializable(MANGA_KEY) as Manga
private val newManga: Manga = args.getSerializable(NEW_MANGA_KEY) as Manga
private var callingController: Controller? = null
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val prefValue = preferences.migrateFlags().get() val prefValue = preferences.migrateFlags().get()
val callingController = callingController
val preselected = val preselected =
MigrationFlags.getEnabledFlagsPositions( MigrationFlags.getEnabledFlagsPositions(
@@ -132,7 +154,7 @@ class SearchController(
) )
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.message(R.string.migration_dialog_what_to_include) .title(R.string.migration_dialog_what_to_include)
.listItemsMultiChoice( .listItemsMultiChoice(
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence }, items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
initialSelection = preselected.toIntArray() initialSelection = preselected.toIntArray()
@@ -145,13 +167,27 @@ class SearchController(
preferences.migrateFlags().set(newValue) preferences.migrateFlags().set(newValue)
} }
.positiveButton(R.string.migrate) { .positiveButton(R.string.migrate) {
(targetController as? SearchController)?.migrateManga() if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.migrateManga(manga, newManga)
} }
.negativeButton(R.string.copy) { .negativeButton(R.string.copy) {
(targetController as? SearchController)?.copyManga() if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.copyManga(manga, newManga)
} }
.neutralButton(android.R.string.cancel) .neutralButton(android.R.string.cancel)
} }
companion object {
const val MANGA_KEY = "manga_key"
const val NEW_MANGA_KEY = "new_manga_key"
}
} }
/** /**
@@ -183,4 +219,15 @@ class SearchController(
} }
.launchIn(viewScope) .launchIn(viewScope)
} }
override fun onTitleClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
router.pushController(SourceSearchController(manga!!, source, presenter.query).withFadeTransaction())
}
companion object {
const val OLD_MANGA = "old_manga"
const val SOURCES = "sources"
}
} }
@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
class SourceSearchController(
bundle: Bundle
) : BrowseSourceController(bundle) {
constructor(manga: Manga, source: CatalogueSource, searchQuery: String? = null) : this(
bundleOf(
SOURCE_ID_KEY to source.id,
MANGA_KEY to manga,
SEARCH_QUERY_KEY to searchQuery
)
)
private var oldManga: Manga = args.getSerializable(MANGA_KEY) as Manga
private var newManga: Manga? = null
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
newManga = item.manga
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
val dialog =
SearchController.MigrationDialog(oldManga, newManga!!, this)
dialog.targetController = searchController
dialog.showDialog(router)
return true
}
private companion object {
const val MANGA_KEY = "oldManga"
}
}
@@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -43,6 +44,11 @@ class MigrationSourcesController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater) binding = MigrationSourcesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -28,9 +28,14 @@ class MigrationSourcesPresenter(
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> { private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader() val header = SelectionHeader()
return library.asSequence().map { it.source }.toSet() return library
.mapNotNull { if (it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */) sourceManager.getOrStub(it) else null } .groupBy { it.source }
.sortedBy { it.name.toLowerCase() } .filterKeys { it != LocalSource.ID /* SY --> */ && it != MERGED_SOURCE_ID /* SY <-- */ }
.map { SourceItem(it, header) }.toList() .map {
val source = sourceManager.getOrStub(it.key)
SourceItem(source, it.value.size, header)
}
.sortedBy { it.source.name.toLowerCase() }
.toList()
} }
} }
@@ -26,8 +26,8 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
fun bind(item: SourceItem) { fun bind(item: SourceItem) {
val source = item.source val source = item.source
binding.title.text = source.name binding.title.text = "${source.name} (${item.mangaCount})"
binding.subtitle.isVisible = true binding.subtitle.isVisible = source.lang != ""
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang) binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
itemView.post { itemView.post {
@@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information. * @param source Instance of [Source] containing source information.
* @param header The header for this item. * @param header The header for this item.
*/ */
data class SourceItem(val source: Source, val header: SelectionHeader) : data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) { AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/** /**
@@ -10,13 +10,13 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
@@ -39,12 +39,7 @@ import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.ui.smartsearch.SmartSearchController import exh.ui.smartsearch.SmartSearchController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -55,7 +50,7 @@ import uy.kohesive.injekt.api.get
* [SourceAdapter.OnLatestClickListener] call function data on latest item click * [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/ */
class SourceController(bundle: Bundle? = null) : class SourceController(bundle: Bundle? = null) :
NucleusController<SourceMainControllerBinding, SourcePresenter>(bundle), SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(bundle),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnSourceClickListener, SourceAdapter.OnSourceClickListener,
@@ -102,6 +97,11 @@ class SourceController(bundle: Bundle? = null) :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater) binding = SourceMainControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -203,7 +203,7 @@ class SourceController(bundle: Bundle? = null) :
items.add( items.add(
Pair( Pair(
activity.getString(R.string.label_categories), activity.getString(R.string.categories),
{ addToCategories(item.source) } { addToCategories(item.source) }
) )
) )
@@ -333,44 +333,6 @@ class SourceController(bundle: Bundle? = null) :
} }
// SY <-- // SY <--
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.source_main, menu)
// SY -->
if (mode == Mode.SMART_SEARCH) {
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_settings).isVisible = false
}
// SY <--
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(viewScope)
}
private fun performGlobalSearch(query: String) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
/** /**
* Called when an option menu item has been selected by the user. * Called when an option menu item has been selected by the user.
* *
@@ -431,6 +393,29 @@ class SourceController(bundle: Bundle? = null) :
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (mode == Mode.CATALOGUE) {
createOptionsMenu(
menu,
inflater,
R.menu.source_main,
R.id.action_search,
R.string.action_global_search_hint,
false // GlobalSearch handles the searching here
)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
// SY -->
if (mode == Mode.CATALOGUE) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
// SY <--
}
// SY --> // SY -->
@Parcelize @Parcelize
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable data class SmartSearchConfig(val origTitle: String, val origMangaId: Long? = null) : Parcelable
@@ -8,7 +8,6 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@@ -17,10 +16,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.input.input import com.afollestad.materialdialogs.input.input
import com.afollestad.materialdialogs.list.listItems import com.afollestad.materialdialogs.list.listItems
import com.elvishew.xlog.XLog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@@ -38,7 +37,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController import eu.kanade.tachiyomi.ui.browse.extension.details.SourcePreferencesController
import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController
@@ -57,25 +56,22 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.EmptyView
import exh.log.xLogW
import exh.md.similar.ui.EnableMangaDexSimilarDialogController import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
* Controller to manage the catalogues available in the app. * Controller to manage the catalogues available in the app.
*/ */
open class BrowseSourceController(bundle: Bundle) : open class BrowseSourceController(bundle: Bundle) :
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle), SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController, FabController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
@@ -389,6 +385,11 @@ open class BrowseSourceController(bundle: Bundle) :
actionFab?.shrinkOnScroll(recycler) actionFab?.shrinkOnScroll(recycler)
} }
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
recycler.adapter = adapter recycler.adapter = adapter
@@ -401,25 +402,8 @@ open class BrowseSourceController(bundle: Bundle) :
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.source_browse, menu) createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search) val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { searchWithQuery(it.queryText.toString()) }
.launchIn(viewScope)
searchItem.fixExpand( searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() }, onExpand = { invalidateMenuOnExpand() },
@@ -450,6 +434,10 @@ open class BrowseSourceController(bundle: Bundle) :
// SY <-- // SY <--
} }
override fun onSearchViewQueryTextSubmit(query: String?) {
searchWithQuery(query ?: "")
}
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
@@ -542,8 +530,8 @@ open class BrowseSourceController(bundle: Bundle) :
*/ */
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) { /* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
// SY --> // SY -->
XLog.tag("BrowseSourceController").enableStackTrace(2).w("> Failed to load next catalogue page!", error) xLogW("> Failed to load next catalogue page!", error)
XLog.tag("BrowseSourceController").enableStackTrace(2).w( xLogW(
"> (source.id: %s, source.name: %s)", "> (source.id: %s, source.name: %s)",
presenter.source.id, presenter.source.id,
presenter.source.name presenter.source.name
@@ -81,12 +81,6 @@ open class BrowseSourcePresenter(
*/ */
lateinit var source: CatalogueSource lateinit var source: CatalogueSource
/**
* Query from the view.
*/
var query = searchQuery ?: ""
private set
/** /**
* Modifiable list of filters. * Modifiable list of filters.
*/ */
@@ -129,6 +123,10 @@ open class BrowseSourcePresenter(
private val filterSerializer = FilterSerializer() private val filterSerializer = FilterSerializer()
// SY <-- // SY <--
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@@ -7,7 +7,6 @@ import android.widget.AutoCompleteTextView
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.elvishew.xlog.XLog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -17,6 +16,7 @@ import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.AutoCompleteAdapter import eu.kanade.tachiyomi.widget.AutoCompleteAdapter
import exh.log.xLogD
open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() { open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<AutoComplete.Holder>() {
@@ -97,7 +97,7 @@ open class AutoComplete(val filter: Filter.AutoComplete) : AbstractFlexibleItem<
addChipToGroup(name, holder) addChipToGroup(name, holder)
filter.state += name filter.state += name
} else { } else {
XLog.tag("AutoComplete").d("Invalid tag: $name") xLogD("Invalid tag: %s", name)
} }
} }
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -10,7 +11,6 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() { open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
@@ -25,11 +25,9 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) { override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.wrapper.hint = filter.name holder.wrapper.hint = filter.name
holder.edit.setText(filter.state) holder.edit.setText(filter.state)
holder.edit.addTextChangedListener(object : SimpleTextWatcher() { holder.edit.doOnTextChanged { text, _, _, _ ->
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) { filter.state = text.toString()
filter.state = text.toString() }
}
})
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -10,20 +10,16 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@@ -33,8 +29,9 @@ import uy.kohesive.injekt.injectLazy
*/ */
open class GlobalSearchController( open class GlobalSearchController(
protected val initialQuery: String? = null, protected val initialQuery: String? = null,
protected val extensionFilter: String? = null protected val extensionFilter: String? = null,
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(), bundle: Bundle? = null
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(bundle),
GlobalSearchCardAdapter.OnMangaClickListener, GlobalSearchCardAdapter.OnMangaClickListener,
GlobalSearchAdapter.OnTitleClickListener { GlobalSearchAdapter.OnTitleClickListener {
@@ -45,6 +42,11 @@ open class GlobalSearchController(
*/ */
protected var adapter: GlobalSearchAdapter? = null protected var adapter: GlobalSearchAdapter? = null
/**
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
*/
private var optionsMenuSearchItem: MenuItem? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@@ -58,6 +60,11 @@ open class GlobalSearchController(
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = GlobalSearchControllerBinding.inflate(inflater) binding = GlobalSearchControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -100,36 +107,32 @@ open class GlobalSearchController(
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu. createOptionsMenu(
inflater.inflate(R.menu.global_search, menu) menu,
inflater,
// Initialize search menu R.menu.global_search,
val searchItem = menu.findItem(R.id.action_search) R.id.action_search,
val searchView = searchItem.actionView as SearchView null,
searchView.maxWidth = Int.MAX_VALUE false // the onMenuItemActionExpand will handle this
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
) )
searchView.queryTextEvents() optionsMenuSearchItem = menu.findItem(R.id.action_search)
.filterIsInstance<QueryTextEvent.QuerySubmitted>() }
.onEach {
presenter.search(it.queryText.toString()) override fun onSearchMenuItemActionExpand(item: MenuItem?) {
searchItem.collapseActionView() super.onSearchMenuItemActionExpand(item)
setTitle() // Update toolbar title val searchView = optionsMenuSearchItem?.actionView as SearchView
} searchView.onActionViewExpanded() // Required to show the query in the view
.launchIn(viewScope)
if (nonSubmittedQuery.isBlank()) {
searchView.setQuery(presenter.query, false)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
presenter.search(query ?: "")
optionsMenuSearchItem?.collapseActionView()
setTitle() // Update toolbar title
} }
/** /**
@@ -48,12 +48,6 @@ open class GlobalSearchPresenter(
*/ */
val sources by lazy { getSourcesToQuery() } val sources by lazy { getSourcesToQuery() }
/**
* Query from the view.
*/
var query = ""
private set
/** /**
* Fetches the different sources by user settings. * Fetches the different sources by user settings.
*/ */
@@ -1,140 +0,0 @@
package eu.kanade.tachiyomi.ui.browse.source.index
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.IndexAdapterBinding
/**
* Adapter that holds the search cards.
*
* @param controller instance of [IndexController].
*/
class IndexAdapter(val controller: IndexController) :
RecyclerView.Adapter<IndexAdapter.ViewHolder>() {
val clickListener: ClickListener = controller
private lateinit var binding: IndexAdapterBinding
var holder: IndexAdapter.ViewHolder? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IndexAdapter.ViewHolder {
binding = IndexAdapterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root)
}
override fun onBindViewHolder(holder: IndexAdapter.ViewHolder, position: Int) {
this.holder = holder
holder.bindBrowse(null)
holder.bindLatest(null)
}
// stores and recycles views as they are scrolled off screen
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val latestAdapter = IndexCardAdapter(controller)
private var latestLastBoundResults: List<IndexCardItem>? = null
private val browseAdapter = IndexCardAdapter(controller)
private var browseLastBoundResults: List<IndexCardItem>? = null
init {
binding.browseBarWrapper.setOnClickListener {
clickListener.onBrowseClick()
}
binding.latestBarWrapper.setOnClickListener {
clickListener.onLatestClick()
}
binding.latestRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
binding.latestRecycler.adapter = latestAdapter
binding.browseRecycler.layoutManager = LinearLayoutManager(itemView.context, LinearLayoutManager.HORIZONTAL, false)
binding.browseRecycler.adapter = browseAdapter
}
fun bindLatest(latestResults: List<IndexCardItem>?) {
when {
latestResults == null -> {
binding.latestProgress.isVisible = true
showLatestResultsHolder()
}
latestResults.isEmpty() -> {
binding.latestProgress.isVisible = false
showLatestNoResults()
}
else -> {
binding.latestProgress.isVisible = false
showLatestResultsHolder()
}
}
if (latestResults !== latestLastBoundResults) {
latestAdapter.updateDataSet(latestResults)
latestLastBoundResults = latestResults
}
}
fun bindBrowse(browseResults: List<IndexCardItem>?) {
when {
browseResults == null -> {
binding.browseProgress.isVisible = true
showBrowseResultsHolder()
}
browseResults.isEmpty() -> {
binding.browseProgress.isVisible = false
showBrowseNoResults()
}
else -> {
binding.browseProgress.isVisible = false
showBrowseResultsHolder()
}
}
if (browseResults !== browseLastBoundResults) {
browseAdapter.updateDataSet(browseResults)
browseLastBoundResults = browseResults
}
}
private fun showLatestResultsHolder() {
binding.latestNoResultsFound.isVisible = false
}
private fun showLatestNoResults() {
binding.latestNoResultsFound.isVisible = true
}
private fun showBrowseResultsHolder() {
binding.browseNoResultsFound.isVisible = false
}
private fun showBrowseNoResults() {
binding.browseNoResultsFound.isVisible = true
}
fun setLatestImage(manga: Manga) {
latestAdapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (latestAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
}
fun setBrowseImage(manga: Manga) {
browseAdapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (browseAdapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
}
}
interface ClickListener {
fun onBrowseClick(search: String? = null, filters: String? = null)
fun onLatestClick()
}
override fun getItemCount(): Int = 1
}
@@ -6,33 +6,29 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.LatestControllerBinding import eu.kanade.tachiyomi.databinding.IndexControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
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.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import reactivecircus.flowbinding.appcompat.QueryTextEvent import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import xyz.nulldev.ts.api.http.serializer.FilterSerializer import xyz.nulldev.ts.api.http.serializer.FilterSerializer
@@ -43,10 +39,9 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer
* [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search * [IndexCardAdapter.OnMangaClickListener] called when manga is clicked in global search
*/ */
open class IndexController : open class IndexController :
NucleusController<LatestControllerBinding, IndexPresenter>, SearchableNucleusController<IndexControllerBinding, IndexPresenter>,
FabController, FabController,
IndexCardAdapter.OnMangaClickListener, IndexCardAdapter.OnMangaClickListener {
IndexAdapter.ClickListener {
constructor(source: CatalogueSource?) : super( constructor(source: CatalogueSource?) : super(
bundleOf( bundleOf(
@@ -65,13 +60,10 @@ open class IndexController :
var source: CatalogueSource? = null var source: CatalogueSource? = null
/** private var latestAdapter: IndexCardAdapter? = null
* Adapter containing search results grouped by lang. private var browseAdapter: IndexCardAdapter? = null
*/
protected var adapter: IndexAdapter? = null
private var actionFab: ExtendedFloatingActionButton? = null private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
/** /**
* Sheet containing filter items. * Sheet containing filter items.
@@ -90,7 +82,7 @@ open class IndexController :
* @return inflated view * @return inflated view
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LatestControllerBinding.inflate(inflater) binding = IndexControllerBinding.inflate(inflater)
return binding.root return binding.root
} }
@@ -134,35 +126,17 @@ open class IndexController :
* @param inflater used to load the menu xml. * @param inflater used to load the menu xml.
*/ */
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu. createOptionsMenu(menu, inflater, R.menu.global_search, R.id.action_search)
inflater.inflate(R.menu.global_search, menu) }
// Initialize search menu override fun onSearchViewQueryTextSubmit(query: String?) {
val searchItem = menu.findItem(R.id.action_search) onBrowseClick(query.nullIfBlank())
val searchView = searchItem.actionView as SearchView }
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query override fun onSearchViewQueryTextChange(newText: String?) {
if (query.isNotBlank()) { if (router.backstack.lastOrNull()?.controller() == this) {
searchItem.expandActionView() presenter.query = newText ?: ""
searchView.setQuery(query, true)
searchView.clearFocus()
} }
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@IndexController }
.onEach {
if (it is QueryTextEvent.QueryChanged) {
presenter.query = it.queryText.toString()
} else if (it is QueryTextEvent.QuerySubmitted) {
onBrowseClick(presenter.query.nullIfBlank())
}
}
.launchIn(viewScope)
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() }
)
} }
/** /**
@@ -176,11 +150,39 @@ open class IndexController :
// Prepare filter sheet // Prepare filter sheet
initFilterSheet() initFilterSheet()
adapter = IndexAdapter(this) latestAdapter = IndexCardAdapter(this)
// Create recycler and set adapter. binding.latestRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
binding.recycler.layoutManager = LinearLayoutManager(view.context) binding.latestRecycler.adapter = latestAdapter
binding.recycler.adapter = adapter
browseAdapter = IndexCardAdapter(this)
binding.browseRecycler.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.HORIZONTAL, false)
binding.browseRecycler.adapter = browseAdapter
binding.latestBarWrapper.clicks()
.onEach {
onLatestClick()
}
.launchIn(viewScope)
binding.browseBarWrapper.clicks()
.onEach {
onBrowseClick()
}
.launchIn(viewScope)
presenter.latestItems
.onEach {
bind(it, true)
}
.launchIn(viewScope)
presenter.browseItems
.onEach {
bind(it, false)
}
.launchIn(viewScope)
presenter.getLatest() presenter.getLatest()
} }
@@ -261,28 +263,64 @@ open class IndexController :
override fun cleanupFab(fab: ExtendedFloatingActionButton) { override fun cleanupFab(fab: ExtendedFloatingActionButton) {
fab.setOnClickListener(null) fab.setOnClickListener(null)
actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) }
actionFab = null actionFab = null
} }
fun setLatestManga(results: List<IndexCardItem>?) { private fun bind(results: List<IndexCardItem>?, isLatest: Boolean) {
adapter?.holder?.bindLatest(results) val progress = if (isLatest) binding.latestProgress else binding.browseProgress
when {
results == null -> {
progress.isVisible = true
showResultsHolder(isLatest)
}
results.isEmpty() -> {
progress.isVisible = false
showNoResults(isLatest)
}
else -> {
progress.isVisible = false
showResultsHolder(isLatest)
}
}
val adapter = if (isLatest) {
latestAdapter
} else {
browseAdapter
}
adapter?.updateDataSet(results)
} }
fun setBrowseManga(results: List<IndexCardItem>?) { fun onError(e: Exception, isLatest: Boolean) {
adapter?.holder?.bindBrowse(results) e.message?.let {
val textView = if (isLatest) {
binding.latestNoResultsFound
} else {
binding.browseNoResultsFound
}
textView.text = it
}
}
private fun showResultsHolder(isLatest: Boolean) {
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = false
}
private fun showNoResults(isLatest: Boolean) {
(if (isLatest) binding.latestNoResultsFound else binding.browseNoResultsFound).isVisible = true
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
adapter = null latestAdapter = null
browseAdapter = null
super.onDestroyView(view) super.onDestroyView(view)
} }
override fun onBrowseClick(search: String?, filters: String?) { fun onBrowseClick(search: String? = null, filters: String? = null) {
router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction()) router.replaceTopController(BrowseSourceController(presenter.source, search, filterList = filters).withFadeTransaction())
} }
override fun onLatestClick() { private fun onLatestClick() {
router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction()) router.replaceTopController(LatestUpdatesController(presenter.source).withFadeTransaction())
} }
@@ -292,8 +330,14 @@ open class IndexController :
* @param manga the initialized manga. * @param manga the initialized manga.
*/ */
fun onMangaInitialized(manga: Manga, isLatest: Boolean) { fun onMangaInitialized(manga: Manga, isLatest: Boolean) {
if (isLatest) adapter?.holder?.setLatestImage(manga) val adapter = if (isLatest) latestAdapter else browseAdapter
else adapter?.holder?.setBrowseImage(manga) adapter ?: return
adapter.allBoundViewHolders.forEach {
if (it !is IndexCardHolder) return@forEach
if (adapter.getItem(it.bindingAdapterPosition)?.manga?.id != manga.id) return@forEach
it.setImage(manga)
}
} }
companion object { companion object {
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.JsonSavedSearch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -52,11 +53,6 @@ open class IndexPresenter(
*/ */
private var fetchSourcesSubscription: Subscription? = null private var fetchSourcesSubscription: Subscription? = null
/**
* Query from the view.
*/
var query = ""
/** /**
* Subject which fetches image of given manga. * Subject which fetches image of given manga.
*/ */
@@ -78,6 +74,14 @@ open class IndexPresenter(
*/ */
private var fetchImageSubscription: Subscription? = null private var fetchImageSubscription: Subscription? = null
val latestItems = MutableStateFlow<List<IndexCardItem>?>(null)
val browseItems = MutableStateFlow<List<IndexCardItem>?>(null)
init {
query = ""
}
override fun onDestroy() { override fun onDestroy() {
fetchSourcesSubscription?.unsubscribe() fetchSourcesSubscription?.unsubscribe()
fetchImageSubscription?.unsubscribe() fetchImageSubscription?.unsubscribe()
@@ -98,54 +102,43 @@ open class IndexPresenter(
initializeFetchImageSubscription() initializeFetchImageSubscription()
presenterScope.launch(Dispatchers.IO) { presenterScope.launch(Dispatchers.IO) {
withUIContext { if (latestItems.value != null) return@launch
Observable.just(null).subscribeLatestCache({ view, results -> val results = if (source.supportsLatest) {
view.setLatestManga(results) try {
})
}
if (source.supportsLatest) {
val results = try {
source.fetchLatestUpdates(1) source.fetchLatestUpdates(1)
.awaitSingle() .awaitSingle()
.mangas .mangas
.take(10)
.map { networkToLocalManga(it, source.id) } .map { networkToLocalManga(it, source.id) }
} catch (e: Exception) { } catch (e: Exception) {
withUIContext {
view?.onError(e, true)
}
emptyList() emptyList()
} }
fetchImage(results, true) } else emptyList()
withUIContext { fetchImage(results, true)
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
view.setLatestManga(results) latestItems.value = results.map { IndexCardItem(it) }
})
}
}
} }
presenterScope.launch(Dispatchers.IO) { presenterScope.launch(Dispatchers.IO) {
withUIContext { if (browseItems.value != null) return@launch
Observable.just(null).subscribeLatestCache({ view, results ->
view.setBrowseManga(results)
})
}
val results = try { val results = try {
source.fetchPopularManga(1) source.fetchPopularManga(1)
.awaitSingle() .awaitSingle()
.mangas .mangas
.take(10)
.map { networkToLocalManga(it, source.id) } .map { networkToLocalManga(it, source.id) }
} catch (e: Exception) { } catch (e: Exception) {
withUIContext {
view?.onError(e, true)
}
emptyList() emptyList()
} }
fetchImage(results, false) fetchImage(results, false)
withUIContext { browseItems.value = results.map { IndexCardItem(it) }
Observable.just(results.map { IndexCardItem(it) }).subscribeLatestCache({ view, results ->
view.setBrowseManga(results)
})
}
} }
} }
@@ -221,10 +214,10 @@ open class IndexPresenter(
fun loadSearches(): List<EXHSavedSearch> { fun loadSearches(): List<EXHSavedSearch> {
val loaded = preferences.savedSearches().get() val loaded = preferences.savedSearches().get()
return loaded.map { return loaded.mapNotNull {
try { try {
val id = it.substringBefore(':').toLong() val id = it.substringBefore(':').toLong()
if (id != source.id) return@map null if (id != source.id) return@mapNotNull null
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':')) val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
val originalFilters = source.getFilterList() val originalFilters = source.getFilterList()
filterSerializer.deserialize(originalFilters, content.filters) filterSerializer.deserialize(originalFilters, content.filters)
@@ -239,6 +232,6 @@ open class IndexPresenter(
t.printStackTrace() t.printStackTrace()
null null
} }
}.filterNotNull() }
} }
} }
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -75,6 +76,11 @@ class CategoryController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -75,6 +76,11 @@ class BiometricTimesController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -5,9 +5,9 @@ import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.datetime.timePicker import com.afollestad.materialdialogs.datetime.timePicker
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import exh.log.xLogD
import java.util.Calendar import java.util.Calendar
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -48,9 +48,9 @@ class BiometricTimesCreateDialog<T>(bundle: Bundle? = null) : DialogController(b
.title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time) .title(if (startTime == null) R.string.biometric_lock_start_time else R.string.biometric_lock_end_time)
.timePicker(show24HoursView = false) { _, datetime -> .timePicker(show24HoursView = false) { _, datetime ->
val hour = datetime.get(Calendar.HOUR_OF_DAY) val hour = datetime.get(Calendar.HOUR_OF_DAY)
XLog.disableStackTrace().d(hour) xLogD(hour)
val minute = datetime.get(Calendar.MINUTE) val minute = datetime.get(Calendar.MINUTE)
XLog.disableStackTrace().d(minute) xLogD(minute)
if (hour !in 0..24 || minute !in 0..60) return@timePicker if (hour !in 0..24 || minute !in 0..60) return@timePicker
if (startTime != null) { if (startTime != null) {
endTime = hour.hours + minute.minutes endTime = hour.hours + minute.minutes
@@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.ui.category.biometric package eu.kanade.tachiyomi.ui.category.biometric
import android.os.Bundle import android.os.Bundle
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import exh.log.xLogD
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import rx.Observable import rx.Observable
@@ -36,7 +36,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges -> preferences.biometricTimeRanges().asFlow().onEach { prefTimeRanges ->
timeRanges = prefTimeRanges.toList() timeRanges = prefTimeRanges.toList()
.mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { XLog.disableStackTrace().d(it) } .mapNotNull { TimeRange.fromPreferenceString(it) }.onEach { xLogD(it) }
Observable.just(timeRanges) Observable.just(timeRanges)
.map { it.map(::BiometricTimesItem) } .map { it.map(::BiometricTimesItem) }
@@ -57,7 +57,7 @@ class BiometricTimesPresenter : BasePresenter<BiometricTimesController>() {
return return
} }
XLog.disableStackTrace().d(timeRange) xLogD(timeRange)
preferences.biometricTimeRanges() += timeRange.toPreferenceString() preferences.biometricTimeRanges() += timeRange.toPreferenceString()
} }
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -81,6 +82,11 @@ class SortTagController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -72,6 +73,11 @@ class RepoController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -79,6 +79,6 @@ class RepoPresenter(
} }
companion object { companion object {
val repoRegex = """^[a-zA-Z-_.]*?\/[a-zA-Z-_.]*?$""".toRegex() val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex()
} }
} }
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -73,6 +74,11 @@ class SourceCategoryController :
*/ */
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater) binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }
@@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@@ -56,6 +57,11 @@ class DownloadController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater) binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root return binding.root
} }

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