Compare commits

...

239 Commits

Author SHA1 Message Date
f590378761 Release 0.10.9 2021-02-12 16:01:23 -05:00
f5f592be91 Require minimum WebView v88, try to catch fatal errors too 2021-02-12 12:42:33 -05:00
7a373fb43a Minor download icon optimizations 2021-02-12 12:27:40 -05:00
aded11e599 Make backup restoring logic more sequential 2021-02-12 12:27:40 -05:00
41d7cee020 Remove ExperimentalSerializationApi opt-in annotations 2021-02-12 12:27:40 -05:00
f2ef6a20e6 Weblate translations (#4378)
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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 <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: 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: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.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: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@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/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
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/fi/
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/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/ms/
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/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: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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 <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: 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: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.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: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
2021-02-12 12:27:32 -05:00
a398c3fb81 Handle link for multisource extension commits (closes #4432) 2021-02-11 17:35:15 -05:00
2a454b44cc Adjust some scopes 2021-02-09 19:14:38 -05:00
7b66ece895 Fix invisible overflow icon in chapter filter sheet in light blue theme 2021-02-09 19:12:44 -05:00
b5017eebbf Added dual page split setting (#4252)
* Add DualPageSplit option

* remove extra line

* Split double-page into two pages

* Remove !isAnimated check and add (ALPHA) to the label

* Fix missing insert pages

* Pager cleanup

* Add dual split to Webtoon and fix Vertical

* Fix L2R/R2L

* Add comments and refactor code in ImageUtil

* Use a simpler split solution in webtoon mode

Co-authored-by: weng <>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
2021-02-09 17:54:44 -05:00
aa67229daf Add weekly to library update frequency options (closes #4422) 2021-02-09 17:49:02 -05:00
5af68186d6 Clean up LibraryUpdateService a bit 2021-02-09 17:44:22 -05:00
545bc0e605 Open manga when clicking thumbnail in migration list (closes #4152) 2021-02-08 17:47:44 -05:00
291168f4de Remove unnecessary LayoutContainer implementations 2021-02-08 17:45:42 -05:00
9facb51f22 Add action to directly share crash log file from notification 2021-02-07 23:05:13 -05:00
5b7d8c5e37 Show locales in list of sources to migrate 2021-02-07 22:54:13 -05:00
5945937e4b Update AboutLibraries 2021-02-07 22:51:23 -05:00
9f9f9872eb Fix legacy backups
(cherry picked from commit ded58541f5903c109b70799683829e26018d2af6)
2021-02-07 22:33:07 -05:00
3566072f4a Revert attempt to programmatically determine user agent string; fallback to Edge 2021-02-07 17:54:28 -05:00
b85cd86b24 Add Esperanto locale 2021-02-07 16:55:44 -05:00
79c3767fff Chapter backup optimization
From fc6d9aaf51 (diff-9872ccc3c9af14d2872ec99199409e60a11cb754ab23e733b1d45843778f7c95R24)
2021-02-07 16:20:07 -05:00
cf1609a429 Massage user agent string from WebView a bit more 2021-02-07 16:19:13 -05:00
3aeac7e7b5 Fix selected tab in sheets not being the accent color 2021-02-07 10:54:35 -05:00
1557f713f4 Don't restrict filter sheet height anymore 2021-02-07 10:49:08 -05:00
b63d24ac1a Add Right and Left navigation (#4392)
and remove default navigation classes in favor of the navigation classes
2021-02-06 23:26:56 -05:00
348c1ff29d Avoid some unnecessary re-renderings of download icons 2021-02-06 23:25:39 -05:00
717e55497f Fix downloads getting deleted when marked as unread 2021-02-06 22:48:06 -05:00
d84b5e8b46 Show help action when source fails to load 2021-02-06 13:09:56 -05:00
5f9ddf9ff5 Use AndroidX version of ContextThemeWrapper 2021-02-06 12:51:40 -05:00
bbee093c63 Remove some logic around old legacy backup versions + minor optimizations 2021-02-06 12:15:34 -05:00
e8c35ae4e1 Do a regular return to cancel update jobs instead of throwing an exception 2021-02-06 12:14:55 -05:00
1607658c30 Set clip data when sharing content URIs (closes #4198) 2021-02-06 09:43:33 -05:00
2e9ef373f3 Minor optimizations for restoring full backups
Based on fc6d9aaf51
2021-02-06 09:32:00 -05:00
ec6eef6d37 Switch back to new image decoder for preview builds 2021-02-06 09:31:18 -05:00
45a19d15ec Release 0.10.8 2021-02-06 09:09:59 -05:00
7191552126 Avoid crash when changing spinner indeterminate state 2021-02-05 23:13:57 -05:00
cfa07490e5 Update dependencies 2021-02-05 22:59:29 -05:00
ae40990eb9 Update to Gradle 6.8.2 2021-02-05 22:59:06 -05:00
9f2fe33ce0 Fix unreadable sheet tab text in light blue theme 2021-02-04 09:16:37 -05:00
33660de6b1 Remove buildSrc module's dependency on JCenter 2021-02-04 09:01:56 -05:00
13d25e0849 Don't automatically set MAL start date (closes #4349) 2021-02-01 22:39:41 -05:00
6662e2002f Rounded bottom sheets 2021-02-01 22:23:29 -05:00
d4081dc899 Remove strings that shouldn't have been translated 2021-01-31 14:59:15 -05:00
62dffb8226 Pad trackers list a bit 2021-01-31 14:52:40 -05:00
cb6aa18480 Weblate translations (#4346)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Byron Vanstien <byronvanstien@rocketmail.com>
Co-authored-by: C201 <derasetad@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: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nestor A. Sanchez <help.toastcode@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: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
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/fr/
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/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
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/sk/
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/
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: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Byron Vanstien <byronvanstien@rocketmail.com>
Co-authored-by: C201 <derasetad@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: Flamm <robindevaux25@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nestor A. Sanchez <help.toastcode@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: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2021-01-31 14:52:32 -05:00
d5cfbef42b Minor cleanup 2021-01-31 14:46:35 -05:00
535abcbb8b Move tracking to a bottom sheet (#4364)
* Move tracking to a bottom sheet

* Give methods better names and remove unnecessary annotation
2021-01-31 14:43:43 -05:00
c34b548a3e Add manga count to the library header (#3884)
* Add manga count to the library header

* Make showing the number of manga configurable

Co-authored-by: arkon <arkon@users.noreply.github.com>
2021-01-31 10:15:37 -05:00
9bf452856c Regenerate drawables (#4352) 2021-01-31 09:55:28 -05:00
17109ab760 Handle failures when updating metadata in library updater 2021-01-29 22:14:17 -05:00
6bc6e1a1d1 Remove unused dimen values 2021-01-28 09:13:05 -05:00
7eef4f7fbf Apply bottom sheet dialog with restriction consistently 2021-01-28 09:10:15 -05:00
75bec6a8e3 Update AndroidX dependencies 2021-01-28 09:03:55 -05:00
0a10f66053 Unify history and update item (#4361)
* Unify history and update item

* Use card_radius
2021-01-27 18:28:02 -05:00
58860b51a2 Ignore failures when updating metadata as part of library update 2021-01-27 17:51:02 -05:00
3ee652b61a Disable Acra crash logs in dev builds 2021-01-27 09:17:40 -05:00
426ed7308b Add comments to not translate the word "WebView" 2021-01-26 17:58:36 -05:00
0ecfef3f70 Address unit test compilation errors
They don't actually run since they broke a long time ago (AndroidX + Roboelectric issues?), but it addresses the annoying red squigglies in Android Studio at least.
2021-01-26 09:05:15 -05:00
5f7e34b6a1 Update Okio, use more KTX stuff (#4353)
* Update Okio to 2.10.0

* Use some more KTX extensions
2021-01-26 09:02:53 -05:00
34cb24fe34 Update total number of chapters when refreshing MAL entries (fixes #4348) 2021-01-24 16:58:23 -05:00
1490112135 Parse correct object when finding existing MAL list item (fixes #4347) 2021-01-24 16:33:47 -05:00
c4716a3f4c Fix cancelling library updates not working 2021-01-24 16:11:45 -05:00
0a54901eb0 Refactor tracker name strings 2021-01-24 16:06:28 -05:00
fea2e0a265 Extend track filter (#4344)
* Allow to filter for each tracker logged in

* Simplify filter logic

* Use variable names instead of it

and rename variables

* Change how trackFilters and items are setup

* Use variable name instead of it and try cleanup filterFnTracking

* Changes from feedback
2021-01-24 15:24:00 -05:00
d3c087375b Weblate translations (#4204)
Co-authored-by: ARiyou Jahan <AR.Jahan2000@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Ciavola Pennelli <loxli91@gmail.com>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jozef Hollý <j2.00ghz@gmail.com>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Sl3iN <fddf.ddrf2015@ya.ru>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: arkon <eugcheung94@gmail.com>
Co-authored-by: waquack <idragonus@gmail.com>
Co-authored-by: Николаев Дмитрий <nikolaevddv@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
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/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/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
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/sah/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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: ARiyou Jahan <AR.Jahan2000@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Ciavola Pennelli <loxli91@gmail.com>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Sl3iN <fddf.ddrf2015@ya.ru>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: arkon <eugcheung94@gmail.com>
Co-authored-by: waquack <idragonus@gmail.com>
Co-authored-by: Николаев Дмитрий <nikolaevddv@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2021-01-24 12:08:01 -05:00
a93c0577ac Minor cleanup 2021-01-24 11:04:25 -05:00
e4dc35674d Perform metadata update in global scope 2021-01-24 10:33:29 -05:00
8a668ba7b9 Revert async metadata updates to fix lag when updating (fixes #4341) 2021-01-23 19:26:21 -05:00
ee9a68b040 Point to preview release for what's new 2021-01-23 17:43:25 -05:00
78e8d40649 Update AboutLibraries plugin 2021-01-23 17:21:39 -05:00
660e13b701 Remove some unused rx/coroutine bridge code 2021-01-23 17:18:43 -05:00
0685382083 Extract user agent string from WebView 2021-01-23 15:57:59 -05:00
04a993c997 Update trackers in parallel, update manga metadata asynchronously 2021-01-23 14:55:00 -05:00
7cae3095c4 Minor dependency updates 2021-01-23 13:23:49 -05:00
e288bf902b Reword download chapter number sorting options 2021-01-23 13:19:57 -05:00
a083e1f71a add sort by chapter number in download queue (#4337)
* add sort by chapter number in download queue

* Bigest, smallest chapter download

* grouped
2021-01-23 13:10:57 -05:00
86b9d7e843 Remove usage of RxJava from LibraryUpdateService 2021-01-23 11:20:16 -05:00
628bd5d6b4 add source name in download queue (#4338) 2021-01-23 10:15:30 -05:00
00285a782c Ignore error when cover is missing in Kitsu search results (fixes #4334) 2021-01-23 10:14:43 -05:00
16be469ecb Surface tracking search errors properly 2021-01-22 22:47:01 -05:00
fdcbc4cffa Add QUERY_ALL_PACKAGES permission (maybe fixes #4313) 2021-01-22 18:03:58 -05:00
fc548304cf Remove unused RECEIVE_BOOT_COMPLETED permission
Was originally added for the library check mechanism: fcb5bf4dd4
2021-01-22 18:03:36 -05:00
7c7ff8165e Don't stop downloader after deleting downloads if it wasn't running (fixes #4309) 2021-01-22 17:52:10 -05:00
496a476c13 Fix multi-select phantom anchor bug in manga chapters and library (#4201)
* Fix phantom anchor bug in manga chapters list when multi-selecting

* Fix phantom bug when long pressing selected items not at top of stack

* Fix phantom anchor bug in library page
2021-01-22 17:35:51 -05:00
441fc6e45b Match color for download, header, and pin buttons (#4331)
* Match download button color to pin color

* Match manga header buttons color to pin color
2021-01-22 09:10:58 -05:00
cf7ec6aa76 Use proper method to clear notification actions 2021-01-21 18:21:21 -05:00
db2dd4b6c6 Add method for users to save error logs to a file 2021-01-21 18:21:03 -05:00
a68417a0b0 Update AGP for Android Studio 4.1.2 2021-01-21 18:05:36 -05:00
2a5102a457 Fix Kitsu toasting "Logged in" when there is an error (#4329) 2021-01-21 18:05:22 -05:00
837d8f5f30 Delay restoring AppBar elevation on Android 5.0 (fixes #4311) 2021-01-18 17:24:23 -05:00
1a5858e99b Hide tracking when no tracker is logged in and change filter logic (#4310)
* Hide tracking when not logged in

* Change string name and value
2021-01-18 17:08:11 -05:00
4044427d93 Add shortcut to see commit history for official extensions 2021-01-17 15:57:19 -05:00
f667f85fa5 More consistent injectLazy style 2021-01-17 15:40:26 -05:00
5cddc0c387 Add filter for tracking (#4276)
* Add filter for tracking or not

* Use .any

* Access database only when needed
2021-01-17 15:40:17 -05:00
cbc01dd6f1 [SKIP CI] Update fork change checklist 2021-01-17 14:39:21 -05:00
b820c7debf Remove duplicated info from CONTRIBUTING.md 2021-01-17 14:15:36 -05:00
2bee072cba Add list of things to change in forks to CONTRIBUTING.md 2021-01-17 14:09:42 -05:00
80710b0b94 Move CONTRIBUTING.md to top level 2021-01-17 14:05:40 -05:00
3319ccfd41 Move ACRA endpoint config to build.gradle.kts 2021-01-17 11:09:29 -05:00
878008e93b Reimplement MAL start/end date support 2021-01-16 16:03:19 -05:00
0cd551d4fd Revert removal of tracker start/end date 2021-01-16 15:48:20 -05:00
f85194ec46 Handle download cancelation from icon properly (fixes #4241) 2021-01-16 15:48:02 -05:00
271489bdfd Fix display mode not updating in source view 2021-01-16 11:30:27 -05:00
bd5f22a049 Update Material Components (fixes #4251) 2021-01-16 10:51:35 -05:00
189f18b112 Simplify lookup for existing MAL list item 2021-01-16 10:51:17 -05:00
df166184ea Update AndroidX dependencies 2021-01-16 10:51:17 -05:00
ce42cba096 Fix jpeg decoder when used with high samples 2021-01-15 16:15:56 +01:00
9670863a41 Show error when trying to download chapters from not installed source (closes #4283) 2021-01-10 11:11:53 -05:00
1ae52bd33f Update SubsamplingScaleImageView
Merged with upstream master and disabled Jetifier.
2021-01-10 11:02:21 -05:00
c9cf9cfff0 More coroutine tweaks 2021-01-10 11:01:10 -05:00
2ffbee3db2 Avoid using global scope where appropriate
Also fixes the crash in tracking when an exception is thrown during a refresh.
2021-01-08 18:05:51 -05:00
96b8beb9cd Fix png artifacts & banding when ratio > 1 2021-01-08 15:13:58 +01:00
365b849046 Fix bad cropping of PNGs with alpha channel 2021-01-08 12:12:59 +01:00
8e613d03e3 Address coroutine scope leaks in custom views 2021-01-07 19:16:26 -05:00
b18a794eca Use lifecycleScope directly 2021-01-07 19:12:30 -05:00
c620c924f9 Revert "Use flows instead of relays for extensions loading"
This reverts commit 07e76f35fa.
2021-01-07 19:06:34 -05:00
9db81a5a49 Fix navigation from feedback (#4238)
* Fix navigation from feedback (fixes #4237)

* Add additional enum values to NavigationRegion mainly for PagerViewer

Co-authored-by: arkon <arkon@users.noreply.github.com>
2021-01-07 19:05:38 -05:00
6fb7a85e8a Address more coroutine scope leaks 2021-01-07 18:15:57 -05:00
36f81b4a62 Minor fixes regarding leaks 2021-01-07 15:19:00 +01:00
2caecc01b2 Fix crash when no chapters can't be loaded 2021-01-06 14:21:55 -05:00
dedb8d2d68 Fix a crash with the new decoder when cropping borders of an entirely white or black image 2021-01-06 20:08:31 +01:00
7192b26402 Fixes on the new decoder library 2021-01-06 13:35:40 +01:00
762f5bdc33 Switch to version of SubsamplingScaleImageView with new image decoder
Courtesy of @inorichi.
Branch: https://github.com/tachiyomiorg/subsampling-scale-image-view/commits/tachiyomi-new-decoder

Decoding speed will be a bit slower now due to two passes if crop borders is enabled, but should be more
reliable on colored images and work for black borders. Memory usage (re: leaks) should also be better.
2021-01-05 22:13:48 -05:00
bebb52b4e8 Allow typing in tracker score similar to chapters (closes #2498) 2021-01-04 16:25:37 -05:00
2c9f8bb9ce Revert bad RxJava conversion that prevented tracking data to be updated in UI 2021-01-04 16:25:19 -05:00
efbefabb01 Reword unmetered network restriction for library updates 2021-01-04 16:12:34 -05:00
990fb22d3e Remove usage of RxJava from backup/restore 2021-01-04 15:30:05 -05:00
9b2c22b2d9 Use JDK 11 for build workflow (#4250) 2021-01-04 15:29:53 -05:00
df7e0d2f2f Surface "NSFW" (includes things like ecchi) results in MAL search (closes #4249) 2021-01-04 15:16:08 -05:00
5cfda1b1bf Refactor bridged RxJava/coroutine calls in SearchPresenters 2021-01-04 15:08:36 -05:00
ac9bf1f3ff Refactor bridged RxJava/coroutine calls in MangaPresenter 2021-01-04 14:55:28 -05:00
7eb0868791 Remove use of RxJava from TrackPresenter 2021-01-04 14:47:23 -05:00
8a792e6d76 MAL: add way to search by list items' titles 2021-01-04 14:30:04 -05:00
d8a3692d92 Fix content focusiblity issues when using remote/controllers (closes #3766) 2021-01-04 14:02:23 -05:00
95ce0e39ef Fix malformed extension URLs 2021-01-04 12:14:02 -05:00
17b70ab38c Refactor Kitsu API to remove Retrofit usage 2021-01-04 12:13:14 -05:00
07e76f35fa Use flows instead of relays for extensions loading 2021-01-04 11:09:31 -05:00
a4cab9876a More consistent wording for NSFW warning strings 2021-01-04 10:27:31 -05:00
c06a932c95 Remove some OptIn annotations 2021-01-04 10:22:26 -05:00
7d713b87b1 Fully remove usages of RxJava from tracker classes
TODO: refactor usages to coroutines as well
2021-01-03 23:41:36 -05:00
b1167146c5 Adjust download icon alignment more 2021-01-03 23:37:56 -05:00
2d0a5eb02c Convert more TrackService methods to coroutines 2021-01-03 23:33:21 -05:00
8d68859c2a Change MAL ID search prefix to "id:" 2021-01-03 23:21:45 -05:00
444cefc9a2 Use updated Tachiyomi version of DirectionalViewPager 2021-01-03 23:04:39 -05:00
d0deceabbd Tint downloaded icons with primary color
Makes them more easily distinguishable from other states.
2021-01-03 22:47:09 -05:00
175c1df0b8 Hide scrollbar from compact tags group 2021-01-03 22:46:37 -05:00
9cc6491c2a Adjust padding of text before download icons 2021-01-03 22:45:41 -05:00
710179f4b4 Note valid backup file extensions in error toast 2021-01-03 22:39:09 -05:00
d11c72fd48 Replace global search context menu option with share target 2021-01-03 11:08:23 -05:00
0af505828e Add more padding to download buttons 2021-01-03 11:01:23 -05:00
135cf9960f Minor cleanup 2021-01-03 10:54:09 -05:00
3bf7c74f93 Navigation settings and split invert tapping for webtoon and pager (#4233) 2021-01-03 10:50:38 -05:00
cea4911c4d add date Format (#4236) 2021-01-03 10:47:23 -05:00
54dc01253d Replace usages of fetchChapterList with 1.x getChapterList API 2021-01-01 19:19:11 -05:00
4db9a90da2 Replace usages of fetchMangaDetails with 1.x getMangaDetails API 2021-01-01 19:08:12 -05:00
d69e9034ab Viewer navigation (#3869)
* Viewer navigation

Co-authored-by: Harsh Parekh <h.x.dev@outlook.com>

* Match current reader behavior and add ability to invert it

* A bit of clean up

* Clean up inversion

* Only create navigator when changed

and change tap zone when invertTapping is changed

* Clean up PagerConfig

* Change how Viewer navigation works

* Add Edge Navigation

Co-authored-by: Harsh Parekh <h.x.dev@outlook.com>
2021-01-01 18:41:20 -05:00
71ece73d99 Direct user back to global search when coming from source search results (#4196) 2021-01-01 16:46:44 -05:00
3bb2102eb4 Include source name in backup restore error logs (closes #4230) 2021-01-01 16:43:32 -05:00
b7914909d0 Remove some unnecessary coroutine dispatcher switching 2020-12-31 18:37:43 -05:00
63398fe491 Implement click events for chapter download icons 2020-12-31 18:14:51 -05:00
bf32bf28da Use coroutines instead of rx for some MangaPresenter operations 2020-12-31 17:45:59 -05:00
dcb6bfb18d Remove some unused variables 2020-12-31 16:41:09 -05:00
8f605dc0f6 Adjust chapters filter icon to line up with download icons 2020-12-31 16:22:11 -05:00
47e770948b Fix download progress bar sizing to match icons 2020-12-31 16:19:25 -05:00
9ab29f5b7f Move some build.gradle.kts content around 2020-12-31 11:32:18 -05:00
10bf430ce6 Only include Firebase dependency in standard flavor 2020-12-31 11:32:06 -05:00
67eb4e8180 Convert app build.gradle to Kotlin DSL (#4222) 2020-12-31 11:28:34 -05:00
141f9b7730 Handle last read page being above total page count (fixes #4010) 2020-12-31 11:21:39 -05:00
139a589ad6 Increase per-page limit when searching MAL manga list 2020-12-31 10:58:40 -05:00
591873a185 Update preview build links (fixes #4225) 2020-12-31 10:52:20 -05:00
97a308b114 Minor cleanup of some tracker observables 2020-12-31 10:51:23 -05:00
430714e67f Find existing entry in MAL list when binding 2020-12-31 10:50:50 -05:00
a49adbd09c Add ID search workaround for MAL tracking 2020-12-30 15:08:10 -05:00
3df98d576e Fix crash on updating trackers after reading with no network (closes #4207) 2020-12-29 09:21:18 -05:00
8135136c86 Adjust download icon sizing 2020-12-29 09:15:32 -05:00
cef1c4b8a1 Tweak manga info header backdrop 2020-12-27 17:58:03 -05:00
2e8791a101 Refactor tracker response parsing 2020-12-27 17:46:14 -05:00
0e2b8b10d1 Show download progress in download icons 2020-12-27 16:50:25 -05:00
3cb64669e4 Animate download icon 2020-12-27 16:41:28 -05:00
bc0d32f330 Add Telugu to language setting 2020-12-27 15:50:37 -05:00
0db17beacc Weblate translations (#4173)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Alperen Arslan <slyvioborin@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Damiano Mason <damicricio99@gmail.com>
Co-authored-by: Dams <qashdzn@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: 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: Kurocon <weblate@kurocon.nl>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: MINEJHAZZ <minecrafterngt@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicolas Meunier <minecraft.arsenof@gmail.com>
Co-authored-by: Nikola Perović <nikolaperovicccc@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: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samiul Azam <yeasin.sanny99@gmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: SmolderingGummy <bairamsaieesh@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Xoko14 <xoquinperezb@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: vlad hateg <vhateg@gmail.com>
Co-authored-by: Đỗ Vũ Minh Quang <quacmichael@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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/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/gl/
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/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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/te/
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/vi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Strings
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: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Alperen Arslan <slyvioborin@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Damiano Mason <damicricio99@gmail.com>
Co-authored-by: Dams <qashdzn@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: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: MINEJHAZZ <minecrafterngt@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicolas Meunier <minecraft.arsenof@gmail.com>
Co-authored-by: Nikola Perović <nikolaperovicccc@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: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samiul Azam <yeasin.sanny99@gmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: SmolderingGummy <bairamsaieesh@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Xoko14 <xoquinperezb@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: vlad hateg <vhateg@gmail.com>
Co-authored-by: Đỗ Vũ Minh Quang <quacmichael@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-12-27 15:49:27 -05:00
931efed784 Use master branch for build workflow 2020-12-27 15:38:32 -05:00
6378a41b6d Minor cleanup of UpdatesHolder 2020-12-27 15:34:14 -05:00
23bf7faf9f Hide chapter download icons for local manga 2020-12-27 15:25:40 -05:00
01ff3af63f Add error state to chapter download icons 2020-12-27 10:44:08 -05:00
8f98055e9e Refactor response parsing helper function 2020-12-27 10:22:24 -05:00
84ae61f72c Refactor download states into enum 2020-12-27 10:20:14 -05:00
6dd280205b Initial chapter download icon implementation 2020-12-27 10:13:13 -05:00
1365d553a4 Log exception on invalid download directory issue 2020-12-26 18:05:39 -05:00
61a594493c Remove usage of Retrofit for update check and extensions list 2020-12-26 16:59:24 -05:00
62ab70f889 Remove unnecessary BaseWebViewActivity class 2020-12-26 11:44:21 -05:00
eaccfdde59 Replace circular ProgressBars with Material component 2020-12-26 11:24:10 -05:00
a8e536478c Replace ProgressBars with Material component 2020-12-26 10:40:45 -05:00
e94d5626dd Update dependencies 2020-12-26 10:40:45 -05:00
be3e31ddc4 Try to avoid crashing when unable to get backup filename for some reason 2020-12-26 10:40:45 -05:00
b92b6520cb Reader snackbar dismissible (#4190)
* Make Reading Mode Snackbar dismissible

* Change reader FrameLayout to CoordinatorLayout
2020-12-26 10:40:31 -05:00
ea33179a95 Convert tracker add/update/login methods to coroutines 2020-12-24 17:50:28 -05:00
6fcf6ae1f5 Use coroutines for Bangumi and Shikimori APIs 2020-12-24 17:23:10 -05:00
f2a9247b68 Remove usage of Gson in Kitsu tracker 2020-12-24 17:02:33 -05:00
dc3ed7fffc Use coroutines for Anilist API 2020-12-24 16:55:04 -05:00
271de31d51 Migrate Kitsu API to coroutines and kotlinx.serialization 2020-12-24 16:39:28 -05:00
1268caf3e0 Make OkHttp coroutine calls always throw exceptions on errors 2020-12-24 15:36:57 -05:00
c0cef58e39 Surface MAL HTTP errors properly 2020-12-24 15:09:10 -05:00
d363d205c3 Fix trackers after MAL not updating 2020-12-21 17:31:31 -05:00
2fd5a9e883 Filter out novels from MAL search results 2020-12-21 17:24:35 -05:00
e7ef974a39 Format MAL search result status/type better 2020-12-21 17:24:25 -05:00
0b62fa8b76 Bump versionCode so next build will force MAL logout 2020-12-21 17:10:08 -05:00
2d28750782 Remove License Appendix That Should Have Been Removed Initially (#4158)
http://www.apache.org/licenses/LICENSE-2.0 makes it pretty clear the appendix is supposed to be separate from the rest of the license. I guess inorichi just forget to get rid of it at the end of LICENSES.txt
2020-12-19 18:41:02 -05:00
e2054a0ab7 Update NDK & buildTools, fix deprecated Parcelize (#4157)
* Update buildTools and NDK

* Fix deprecated Parcelize
2020-12-19 18:40:44 -05:00
7ae5c3b2e7 [SKIP CI] Add preview note for MAL rewrite 2020-12-18 23:37:58 -05:00
6e7fefb8b2 Strip out no longer used tracker start/end date code
Unfortunately MAL was the only one that supported it in the app, but the official API doesn't support it, so now no trackers have it.
2020-12-18 23:31:28 -05:00
450bef278b Add forced MAL logout for next release migration 2020-12-18 23:20:42 -05:00
0affc0d58b Migrate to official MyAnimeList API (closes #4140) 2020-12-18 23:18:31 -05:00
3d153b6c8e Don't surface hidden settings when searching 2020-12-15 23:17:20 -05:00
04fff91e23 Update app repo URL 2020-12-15 15:56:16 -05:00
28a23452f2 Update extensions repo URL 2020-12-15 15:48:06 -05:00
6d403851cf Apply theme to OAuth login redirect activities 2020-12-14 23:00:01 -05:00
395a749bce Misc tracker code cleanup 2020-12-14 22:56:33 -05:00
2cc2a90941 Refactor CustomTabsIntent creation 2020-12-13 22:30:27 -05:00
c87ba6231d Release 0.10.7 2020-12-13 21:02:28 -05:00
c5ca739b49 Fix loading fallback thumbnails in browse view (closes #4127) 2020-12-13 20:47:48 -05:00
00fe4cdf2d Minor code cleanup 2020-12-13 18:11:18 -05:00
69be3e1e87 Complete migration off of Kotlin synthetics 2020-12-13 18:01:51 -05:00
2cb3984d68 Show MAL relogin message on update, localize error message 2020-12-13 17:52:21 -05:00
5901978889 Use view binding for date headers 2020-12-13 17:47:29 -05:00
8bf1cf3cc5 Update to coroutines 1.4.2
Should fix crashes on some devices. See https://github.com/Kotlin/kotlinx.coroutines/issues/2371
2020-12-13 17:18:59 -05:00
f6af1184bc Reword NSFW settings section 2020-12-13 17:18:38 -05:00
4880741b8b More crash fixes 2020-12-13 17:18:28 -05:00
e8627800fe Remove bundled fallback file picker 2020-12-13 12:42:10 -05:00
907fbb94a2 Require WebView 86+ 2020-12-13 11:27:05 -05:00
fd2028557e Some crash fixes 2020-12-13 11:00:46 -05:00
91fa1ec6b2 Suppress some deprecation warnings 2020-12-12 23:50:28 -05:00
628c525599 Disable release postprocessing (Proguard)
Caused the following error for someone:

Detected problmes with app native libraries (please consult log for detail):
libavcoded.so: text relocations
libswresample.so:test relocations
2020-12-12 23:50:05 -05:00
bbc00768f0 Note that toggling NSFW sources requires a restart 2020-12-12 17:27:11 -05:00
5b09461ccf Break out NSFW hiding/labeling into separate settings 2020-12-12 17:20:54 -05:00
1a439ecece Remove source overwrite logic since built-in sources no longer exist 2020-12-12 16:15:18 -05:00
836aec4396 Flip left/right key events for Webtoon viewer (fixes #4111) 2020-12-12 15:50:56 -05:00
0b5dec9bab Revert "Hide incomplete NSFW source labelling settings"
This reverts commit a3b1690d38.
2020-12-12 11:32:55 -05:00
fd56123267 [SKIP CI] Update issue templates 2020-12-12 11:32:44 -05:00
415 changed files with 7554 additions and 6100 deletions

View File

@ -1,33 +0,0 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [Feature requests](#feature-requests)
* [Translations](https://tachiyomi.org/help/contribution/#translation)
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
***
# Catalogue requests
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs
* Include version (More > About > Version)
* If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible)
* For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75
# Feature requests
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)

View File

@ -2,9 +2,9 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.5)
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

View File

@ -9,9 +9,9 @@ labels: "bug"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.5)
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

View File

@ -4,5 +4,5 @@ contact_links:
url: https://tachiyomi.org/help/
about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository
url: https://github.com/inorichi/tachiyomi-extensions
url: https://github.com/tachiyomiorg/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead.

View File

@ -9,9 +9,9 @@ labels: "feature"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.5)
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

View File

@ -1,8 +1,8 @@
---
name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
labels: "catalog, invalid"
---
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -2,7 +2,7 @@ name: CI
on:
push:
branches:
- dev
- master
tags:
- v*
pull_request:
@ -34,10 +34,10 @@ jobs:
- name: Clone repo
uses: actions/checkout@v2
- name: Set up JDK 1.8
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Copy CI gradle.properties
run: |
@ -55,14 +55,14 @@ jobs:
# Sign APK and create release for tags
- name: Get tag name
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
id: get_tag_name
run: |
set -x
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Sign APK
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/standard/release
@ -72,7 +72,7 @@ jobs:
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
id: create_release
uses: actions/create-release@v1
env:
@ -84,7 +84,7 @@ jobs:
prerelease: false
- name: Upload APK to release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

34
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,34 @@
Looking to report an issue/bug or make a feature request? Please refer to the [README file](https://github.com/tachiyomiorg/tachiyomi#issues-feature-requests-and-contributing).
---
Thanks for your interest in contributing to Tachiyomi!
# Code contributions
Pull requests are welcome!
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
# Translations
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
# Forks
Forks are allowed so long as they abide by [the project's LICENSE](https://github.com/tachiyomiorg/tachiyomi/blob/master/LICENSE).
When creating a fork, remember to:
- To avoid confusion with the main app:
- Change the app name
- Change the app icon
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
- To avoid installation conflicts:
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
- To avoid having your data polluting the main app's analytics and crash report services:
- If you want to use Firebase analytics, replace [`google-services.json`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/standard/google-services.json) with your own
- If you want to use ACRA crash reporting, replace the `ACRA_URI` endpoint in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts) with your own

26
LICENSE
View File

@ -174,29 +174,3 @@
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,3 +1,6 @@
### r2903
- The MyAnimeList tracker was rewritten. You will need to log out and log in again.
### r1810
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
run properly. This includes app updates, library updates, and automatic backups.

View File

@ -1,6 +1,6 @@
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| ![CI](https://github.com/inorichi/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/inorichi/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/android-app-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/android-app-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
| ![CI](https://github.com/tachiyomiorg/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above.
## Features
Features include:
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Local reading of downloaded manga
* A configurable reader with multiple viewers, reading directions and other settings.
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
@ -21,7 +21,7 @@ Features include:
* Create backups locally to read offline or to your desired cloud service
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
</details>
@ -47,9 +47,9 @@ Please make sure to read the full guidelines. Your issue may be closed without w
* For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75
DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
</details>
@ -58,7 +58,12 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
</details>
<details><summary>Contributing</summary>
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
</details>
## FAQ

View File

@ -1,328 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.github.zellius.shortcut-helper'
shortcutHelper.filePath = './shortcuts.xml'
ext {
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
getCommitCount = {
return 'git rev-list --count HEAD'.execute().text.trim()
// return "1"
}
getGitSha = {
return 'git rev-parse --short HEAD'.execute().text.trim()
// return "1"
}
getBuildTime = {
def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.setTimeZone(TimeZone.getTimeZone("UTC"))
return df.format(new Date())
}
}
android {
compileSdkVersion AndroidConfig.compileSdk
buildToolsVersion AndroidConfig.buildTools
ndkVersion AndroidConfig.ndk
defaultConfig {
applicationId "eu.kanade.tachiyomi"
minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 52
versionName "0.10.6"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
}
}
buildFeatures {
viewBinding = true
}
buildTypes {
debug {
versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug"
}
release {
postprocessing {
obfuscate false
optimizeCode true
removeUnusedCode false
removeUnusedResources true
proguardFiles 'proguard-rules.pro'
}
}
}
flavorDimensions "default"
productFlavors {
standard {
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
dimension "default"
}
fdroid {
dimension "default"
}
dev {
resConfigs "en", "xxhdpi"
dimension "default"
}
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE'
}
dependenciesInfo {
includeInApk = false
}
lintOptions {
disable 'MissingTranslation'
disable 'ExtraTranslation'
abortOnError false
checkReleaseBuilds false
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
androidExtensions {
experimental = true
}
dependencies {
// Source models and interfaces from Tachiyomi 1.x
implementation 'tachiyomi.sourceapi:source-api:1.1'
// AndroidX libraries
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha01'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
final lifecycle_version = '2.3.0-beta01'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Job scheduling
implementation "androidx.work:work-runtime-ktx:2.5.0-beta02"
// UI library
implementation 'com.google.android.material:material:1.3.0-alpha04'
standardImplementation 'com.google.firebase:firebase-core:18.0.0'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client
final okhttp_version = '4.10.0-RC1'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
implementation 'com.squareup.okio:okio:2.9.0'
// TLS 1.3 support for Android < 10
implementation 'org.conscrypt:conscrypt-android:2.5.1'
// REST
final retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON
final kotlin_serialization_version = '1.0.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialization_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlin_serialization_version"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// JavaScript engine
implementation 'com.squareup.duktape:duktape-android:1.3.0'
// Disk
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588'
implementation 'com.github.junrar:junrar:7.4.0'
// HTML parser
implementation 'org.jsoup:jsoup:1.13.1'
// Database
implementation 'androidx.sqlite:sqlite-ktx:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.33.0'
// Preferences
implementation 'com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3'
// Model View Presenter
final nucleus_version = '3.0.0'
implementation "info.android15.nucleus:nucleus:$nucleus_version"
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
implementation "com.github.inorichi.injekt:injekt-core:65b0440"
// Image library
final glide_version = '4.11.0'
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:6caf219'
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports
implementation 'ch.acra:acra-http:5.7.0'
// Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
final material_dialogs_version = '3.1.1'
implementation "com.afollestad.material-dialogs:core:$material_dialogs_version"
implementation "com.afollestad.material-dialogs:input:$material_dialogs_version"
implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version"
// Conductor
implementation 'com.bluelinelabs:conductor:2.1.5'
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude group: "com.android.support"
}
implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
// FlowBinding
final flowbinding_version = '0.12.0'
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Licenses
implementation "com.mikepenz:aboutlibraries:$BuildPluginsVersion.ABOUTLIB_PLUGIN"
// Tests
testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:3.16.1'
testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
testImplementation "org.robolectric:robolectric:$robolectric_version"
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
final coroutines_version = '1.4.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
}
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
}
}
repositories {
mavenCentral()
jcenter()
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
tasks.withType(AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += [
"-Xopt-in=kotlin.Experimental",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
]
}
// Duplicating Hebrew string assets due to some locale code issues on different devices
task copyHebrewStrings(type: Copy) {
from './src/main/res/values-he'
into './src/main/res/values-iw'
include '**/*'
}
preBuild.dependsOn(formatKotlin, copyHebrewStrings)
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services'
}

333
app/build.gradle.kts Normal file
View File

@ -0,0 +1,333 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
plugins {
id("com.android.application")
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper")
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply(plugin = "com.google.gms.google-services")
}
shortcutHelper.setFilePath("./shortcuts.xml")
android {
compileSdkVersion(AndroidConfig.compileSdk)
buildToolsVersion(AndroidConfig.buildTools)
ndkVersion = AndroidConfig.ndk
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 56
versionName = "0.10.9"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
// Please disable ACRA or use your own instance in forked versions of the project
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
multiDexEnabled = true
ndk {
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
}
}
buildFeatures {
viewBinding = true
}
buildTypes {
named("debug") {
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
}
named("release") {
/*named("postprocessing") {
postprocessing {
isObfuscate = false
isOptimizeCode = true
isRemoveUnusedCode = false
isRemoveUnusedResources = true
}
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
}*/
}
}
flavorDimensions("default")
productFlavors {
create("standard") {
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
dimension = "default"
}
create("fdroid") {
dimension = "default"
}
create("dev") {
resConfigs("en", "xxhdpi")
dimension = "default"
}
}
packagingOptions {
exclude("META-INF/DEPENDENCIES")
exclude("LICENSE.txt")
exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE")
}
dependenciesInfo {
includeInApk = false
}
lintOptions {
disable("MissingTranslation", "ExtraTranslation")
isAbortOnError = false
isCheckReleaseBuilds = false
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
// Source models and interfaces from Tachiyomi 1.x
implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries
implementation("androidx.annotation:annotation:1.2.0-beta01")
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-beta01")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.3.0-rc01"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
// Job scheduling
implementation("androidx.work:work-runtime-ktx:2.5.0")
// UI library
implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
// ReactiveX
implementation("io.reactivex:rxandroid:1.2.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client
val okhttpVersion = "4.10.0-RC1"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0")
// TLS 1.3 support for Android < 10
implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON
val kotlinSerializationVersion = "1.0.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// JavaScript engine
implementation("com.squareup.duktape:duktape-android:1.3.0")
// Disk
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
implementation("org.jsoup:jsoup:1.13.1")
// Database
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
implementation("io.requery:sqlite-android:3.33.0")
// Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
// Model View Presenter
val nucleusVersion = "3.0.0"
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
// Dependency injection
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library
val glideVersion = "4.11.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
// Logging
implementation("com.jakewharton.timber:timber:4.7.1")
// Crash reports
implementation("ch.acra:acra-http:5.7.0")
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
implementation("eu.davidea:flexible-adapter:5.1.0")
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1"
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
// Conductor
implementation("com.bluelinelabs:conductor:2.1.5")
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support")
}
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
// FlowBinding
val flowbindingVersion = "0.12.0"
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
// Licenses
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests
testImplementation("junit:junit:4.13.1")
testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19")
val robolectricVersion = "3.1.4"
testImplementation("org.robolectric:robolectric:$robolectricVersion")
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.4.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
}
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
"-Xopt-in=kotlin.Experimental",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
)
}
// Duplicating Hebrew string assets due to some locale code issues on different devices
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
from("./src/main/res/values-he")
into("./src/main/res/values-iw")
include("**/*")
}
preBuild {
dependsOn(formatKotlin, copyHebrewStrings)
}
}
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
}
}
// Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround
fun getCommitCount(): String {
return runCommand("git rev-list --count HEAD")
// return "1"
}
fun getGitSha(): String {
return runCommand("git rev-parse --short HEAD")
// return "1"
}
fun getBuildTime(): String {
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
df.timeZone = TimeZone.getTimeZone("UTC")
return df.format(Date())
}
fun runCommand(command: String): String {
val byteOut = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = byteOut
}
return String(byteOut.toByteArray()).trim()
}

View File

@ -2,16 +2,24 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- To view extension packages in API 30+ -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<application
android:name=".App"
@ -43,7 +51,7 @@
android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/process_text_action_name">
android:label="@string/action_global_search">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -55,7 +63,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
@ -80,10 +88,6 @@
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist">
@ -100,7 +104,18 @@
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:configChanges="uiMode|orientation|screenSize" />
android:label="MyAnimeList">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="myanimelist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori">

View File

@ -20,9 +20,7 @@ import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.security.Security
@AcraCore(
@ -30,30 +28,22 @@ import java.security.Security
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
)
@AcraHttpSender(
uri = "https://tachiyomi.kanade.eu/crash_report",
uri = BuildConfig.ACRA_URI,
httpMethod = HttpSender.Method.PUT
)
open class App : Application(), LifecycleObserver {
private val preferences: PreferencesHelper by injectLazy()
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// Debug tool; see https://fbflipper.com/
// SoLoader.init(this, false)
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
// val client = AndroidFlipperClient.getInstance(this)
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
// client.addPlugin(DatabasesFlipperPlugin(this))
// client.start()
// }
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
setupAcra()
@ -77,14 +67,15 @@ open class App : Application(), LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
@Suppress("unused")
fun onAppBackgrounded() {
val preferences: PreferencesHelper by injectLazy()
if (preferences.lockAppAfter().get() >= 0) {
SecureActivityDelegate.locked = true
}
}
protected open fun setupAcra() {
ACRA.init(this)
if (BuildConfig.FLAVOR != "dev") {
ACRA.init(this)
}
}
protected open fun setupNotificationChannels() {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi
import android.app.Application
import android.os.Handler
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -11,8 +12,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
@ -48,15 +47,16 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { Json { ignoreUnknownKeys = true } }
// Asynchronously init expensive components for a faster cold start
Handler().post {
get<PreferencesHelper>()
GlobalScope.launch { get<PreferencesHelper>() }
get<NetworkHelper>()
GlobalScope.launch { get<NetworkHelper>() }
get<SourceManager>()
GlobalScope.launch { get<SourceManager>() }
get<DatabaseHelper>()
GlobalScope.launch { get<DatabaseHelper>() }
GlobalScope.launch { get<DownloadManager>() }
get<DownloadManager>()
}
}
}

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -92,6 +93,7 @@ object Migrations {
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
@Suppress("DEPRECATION")
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
preferences.librarySortingMode().set(LibrarySort.ALPHA)
}
@ -114,10 +116,16 @@ object Migrations {
putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key"))
remove("pref_filter_completed_key")
}
}
if (oldVersion < 54) {
// Force MAL log out due to login flow change
// v52: switched from scraping to WebView
// v53: switched from WebView to OAuth
val trackManager = Injekt.get<TrackManager>()
trackManager.myAnimeList.logout()
if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
}
return true
}

View File

@ -5,12 +5,13 @@ import android.net.Uri
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class AbstractBackupManager(protected val context: Context) {
@ -31,22 +32,22 @@ abstract class AbstractBackupManager(protected val context: Context) {
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* [Observable] that fetches chapter information
* Fetches chapter information.
*
* @param source source of manga
* @param manga manga that needs updating
* @param chapters list of chapters in the backup
* @return [Observable] that contains manga
* @return Updated manga chapters.
*/
internal fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { (first) ->
if (first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters)
}
}
internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
val fetchedChapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source)
if (syncedChapters.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters)
}
return syncedChapters
}
/**
@ -79,6 +80,13 @@ abstract class AbstractBackupManager(protected val context: Context) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
/**
* Updates a list of chapters with known database ids
*/
protected fun updateKnownChapters(chapters: List<Chapter>) {
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
}
/**
* Return number of backups.
*

View File

@ -10,8 +10,8 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.Job
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.text.SimpleDateFormat
@ -37,9 +37,9 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
protected val errors = mutableListOf<Pair<Date, String>>()
abstract fun performRestore(uri: Uri): Boolean
abstract suspend fun performRestore(uri: Uri): Boolean
fun restoreBackup(uri: Uri): Boolean {
suspend fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
restoreProgress = 0
errors.clear()
@ -58,48 +58,48 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
}
/**
* [Observable] that fetches chapter information
* Fetches chapter information.
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
* @return Updated manga chapters.
*/
internal fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
internal suspend fun updateChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> {
return try {
backupManager.restoreChapters(source, manga, chapters)
} catch (e: Exception) {
// If there's any error, return empty update and continue.
.onErrorReturn {
val errorMessage = if (it is NoChaptersException) {
context.getString(R.string.no_chapters_error)
} else {
it.message
}
errors.add(Date() to "${manga.title} - $errorMessage")
Pair(emptyList(), emptyList())
val errorMessage = if (e is NoChaptersException) {
context.getString(R.string.no_chapters_error)
} else {
e.message
}
errors.add(Date() to "${manga.title} - $errorMessage")
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* Refreshes tracking information.
*
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
internal fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.flatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, service?.name)}")
Observable.empty()
internal suspend fun updateTracking(manga: Manga, tracks: List<Track>) {
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
try {
val updatedTrack = service.refresh(track)
db.insertTrack(updatedTrack).executeAsBlocking()
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
} else {
val serviceName = service?.nameRes()?.let { context.getString(it) }
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, serviceName)}")
}
}
}
/**
@ -120,15 +120,15 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
internal fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val destFile = File(context.externalCacheDir, "tachiyomi_restore.txt")
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
destFile.bufferedWriter().use { out ->
file.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return destFile
return file
}
} catch (e: Exception) {
// Empty

View File

@ -111,7 +111,7 @@ class BackupCreateService : Service() {
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}

View File

@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
}
}
fun showBackupComplete(unifile: UniFile) {
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
with(completeNotificationBuilder) {
@ -68,14 +68,12 @@ class BackupNotifier(private val context: Context) {
setContentText(unifile.filePath ?: unifile.name)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
clearActions()
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
)
show(Notifications.ID_BACKUP_COMPLETE)
@ -94,9 +92,7 @@ class BackupNotifier(private val context: Context) {
setOnlyAlertOnce(true)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
clearActions()
addAction(
R.drawable.ic_close_24dp,
@ -137,16 +133,14 @@ class BackupNotifier(private val context: Context) {
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
clearActions()
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
val destFile = File(path, file)
val uri = destFile.getUriCompat(context)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)

View File

@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
@ -68,12 +71,14 @@ class BackupRestoreService : Service() {
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var ioScope: CoroutineScope
private var backupRestore: AbstractBackupRestore<*>? = null
private lateinit var notifier: BackupNotifier
override fun onCreate() {
super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
@ -92,6 +97,7 @@ class BackupRestoreService : Service() {
private fun destroyJob() {
backupRestore?.job?.cancel()
ioScope?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -122,6 +128,7 @@ class BackupRestoreService : Service() {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
else -> LegacyBackupRestore(this, notifier)
}
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
backupRestore?.writeErrorLog()
@ -129,14 +136,15 @@ class BackupRestoreService : Service() {
notifier.showRestoreError(exception.message)
stopSelf(startId)
}
backupRestore?.job = GlobalScope.launch(handler) {
val job = ioScope.launch(handler) {
if (backupRestore?.restoreBackup(uri) == false) {
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
}
}
backupRestore?.job?.invokeOnCompletion {
job.invokeOnCompletion {
stopSelf(startId)
}
backupRestore?.job = job
return START_NOT_STICKY
}

View File

@ -26,17 +26,16 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.ExperimentalSerializationApi
import eu.kanade.tachiyomi.source.model.toSManga
import kotlinx.serialization.protobuf.ProtoBuf
import okio.buffer
import okio.gzip
import okio.sink
import rx.Observable
import timber.log.Timber
import kotlin.math.max
@OptIn(ExperimentalSerializationApi::class)
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val parser = ProtoBuf
@ -182,29 +181,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
/**
* [Observable] that fetches manga information
* Fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
* @return Updated manga info.
*/
fun restoreMangaFetchObservable(source: Source?, manga: Manga, online: Boolean): Observable<Manga> {
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
return if (online && source != null) {
source.fetchMangaDetails(manga)
.map { networkManga ->
manga.copyFrom(networkManga)
manga.favorite = manga.favorite
manga.initialized = true
manga.id = insertManga(manga)
manga
}
val networkManga = source.getMangaDetails(manga.toMangaInfo())
manga.also {
it.copyFrom(networkManga.toSManga())
it.favorite = manga.favorite
it.initialized = true
it.id = insertManga(manga)
}
} else {
Observable.just(manga)
.map {
it.initialized = it.description != null
it.id = insertManga(it)
it
}
manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
}
}
@ -249,7 +245,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull {
it.order == backupCategoryOrder
@ -276,7 +272,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
// List containing history to be updated
val historyToBeUpdated = mutableListOf<History>()
val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
@ -360,9 +356,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
chapters.forEach { chapter ->
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
if (pos != -1) {
val dbChapter = dbChapters[pos]
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
@ -375,12 +370,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
chapter.bookmark = dbChapter.bookmark
}
}
}
// Filter the chapters that couldn't be found.
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
updateChapters(chapters)
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
@ -388,9 +384,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter ->
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
if (pos != -1) {
val dbChapter = dbChapters[pos]
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
@ -403,10 +398,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
chapter.bookmark = dbChapter.bookmark
}
}
}
chapters.map { it.manga_id = manga.id }
updateChapters(chapters.filter { it.id != null })
insertChapters(chapters.filter { it.id == null })
chapter.manga_id = manga.id
}
val newChapters = chapters.groupBy { it.id != null }
newChapters[true]?.let { updateKnownChapters(it) }
newChapters[false]?.let { insertChapters(it) }
}
}

View File

@ -13,17 +13,14 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.ExperimentalSerializationApi
import okio.buffer
import okio.gzip
import okio.source
import rx.Observable
import java.util.Date
@OptIn(ExperimentalSerializationApi::class)
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
override suspend fun performRestore(uri: Uri): Boolean {
backupManager = FullBackupManager(context)
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
@ -60,23 +57,24 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
val history = backupManga.history
val tracks = backupManga.getTrackingImpl()
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try {
val source = backupManager.sourceManager.get(manga.source)
if (source != null || !online) {
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
} else {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
}
restoreProgress += 1
@ -93,7 +91,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* @param history history data from json
* @param tracks tracking data from json
*/
private fun restoreMangaData(
private suspend fun restoreMangaData(
manga: Manga,
source: Source?,
chapters: List<Chapter>,
@ -119,13 +117,13 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
}
/**
* [Observable] that fetches manga information
* Fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
private suspend fun restoreMangaFetch(
source: Source?,
manga: Manga,
chapters: List<Chapter>,
@ -135,31 +133,25 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
backupCategories: List<BackupCategory>,
online: Boolean
) {
backupManager.restoreMangaFetchObservable(source, manga, online)
.doOnError {
errors.add(Date() to "${manga.title} - ${it.message}")
try {
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
fetchedManga.id ?: return
if (online && source != null) {
updateChapters(source, fetchedManga, chapters)
} else {
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
}
.filter { it.id != null }
.flatMap {
if (online && source != null) {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
} else {
backupManager.restoreChaptersForMangaOffline(it, chapters)
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks, backupCategories)
}
.flatMap {
trackingFetchObservable(it, tracks)
}
.subscribe()
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
}
private fun restoreMangaNoFetch(
private suspend fun restoreMangaNoFetch(
source: Source?,
backupManga: Manga,
chapters: List<Chapter>,
@ -169,27 +161,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
backupCategories: List<BackupCategory>,
online: Boolean
) {
Observable.just(backupManga)
.flatMap { manga ->
if (online && source != null) {
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
} else {
backupManager.restoreChaptersForMangaOffline(manga, chapters)
Observable.just(manga)
}
if (online && source != null) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks, backupCategories)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
}
.subscribe()
} else {
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
updateTracking(backupManga, tracks)
}
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {

View File

@ -5,12 +5,10 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import okio.buffer
import okio.gzip
import okio.source
@OptIn(ExperimentalSerializationApi::class)
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
/**
* Checks for critical backup file data.
@ -41,7 +39,7 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.map { context.getString(it.nameRes()) }
.sorted()
return Results(missingSources, missingTrackers)

View File

@ -44,38 +44,23 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import rx.Observable
import eu.kanade.tachiyomi.source.model.toSManga
import timber.log.Timber
import kotlin.math.max
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
var parserVersion: Int = version
private set
var parser: Gson = initParser()
/**
* Set version of parser
*
* @param version version of parser
*/
internal fun setVersion(version: Int) {
this.parserVersion = version
parser = initParser()
}
private fun initParser(): Gson = when (parserVersion) {
2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
val parser: Gson = when (version) {
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Unknown backup version")
}
@ -249,21 +234,20 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
}
/**
* [Observable] that fetches manga information
* Fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
* @return Updated manga.
*/
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.map { networkManga ->
manga.copyFrom(networkManga)
manga.favorite = true
manga.initialized = true
manga.id = insertManga(manga)
manga
}
suspend fun fetchManga(source: Source, manga: Manga): Manga {
val networkManga = source.getMangaDetails(manga.toMangaInfo())
return manga.also {
it.copyFrom(networkManga.toSManga())
it.favorite = true
it.initialized = true
it.id = insertManga(manga)
}
}
/**
@ -308,7 +292,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
*/
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
if (backupCategoryStr == dbCategory.name) {
@ -332,7 +316,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
*/
internal fun restoreHistoryForManga(history: List<DHistory>) {
// List containing history to be updated
val historyToBeUpdated = mutableListOf<History>()
val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
@ -361,14 +345,14 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
* @param tracks the track list to restore.
*/
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id
tracks.map { it.manga_id = manga.id!! }
// Get tracks from database
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
val trackToUpdate = mutableListOf<Track>()
val trackToUpdate = ArrayList<Track>(tracks.size)
tracks.forEach { track ->
// Fix foreign keys with the current manga id
track.manga_id = manga.id!!
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
@ -423,12 +407,13 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
chapter.copyFrom(dbChapter)
break
}
}
// Filter the chapters that couldn't be found.
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
updateChapters(chapters)
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
}

View File

@ -21,12 +21,11 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.source.Source
import rx.Observable
import java.util.Date
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
override suspend fun performRestore(uri: Uri): Boolean {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
@ -63,7 +62,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(mangaJson: JsonObject) {
private suspend fun restoreManga(mangaJson: JsonObject) {
val manga = backupManager.parser.fromJson<MangaImpl>(
mangaJson.get(
Backup.MANGA
@ -86,16 +85,17 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
?: JsonArray()
)
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try {
val source = backupManager.sourceManager.get(manga.source)
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
}
restoreProgress += 1
@ -112,7 +112,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
* @param history history data from json
* @param tracks tracking data from json
*/
private fun restoreMangaData(
private suspend fun restoreMangaData(
manga: Manga,
source: Source,
chapters: List<Chapter>,
@ -136,13 +136,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
}
/**
* [Observable] that fetches manga information
* Fetches manga information.
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
private suspend fun restoreMangaFetch(
source: Source,
manga: Manga,
chapters: List<Chapter>,
@ -150,27 +150,21 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
history: List<DHistory>,
tracks: List<Track>
) {
backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
manga
}
.filter { it.id != null }
.flatMap {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
}
.subscribe()
try {
val fetchedManga = backupManager.fetchManga(source, manga)
fetchedManga.id ?: return
updateChapters(source, fetchedManga, chapters)
restoreExtraForManga(fetchedManga, categories, history, tracks)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
}
private fun restoreMangaNoFetch(
private suspend fun restoreMangaNoFetch(
source: Source,
backupManga: Manga,
chapters: List<Chapter>,
@ -178,22 +172,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
history: List<DHistory>,
tracks: List<Track>
) {
Observable.just(backupManga)
.flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
}
.subscribe()
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks)
updateTracking(backupManga, tracks)
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {

View File

@ -45,7 +45,7 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.map { context.getString(it.nameRes()) }
.sorted()
return Results(missingSources, missingTrackers)

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -35,12 +35,13 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
}
override fun mapToContentValues(obj: Category) =
contentValuesOf(
COL_ID to obj.id,
COL_NAME to obj.name,
COL_ORDER to obj.order,
COL_FLAGS to obj.flags
)
}
class CategoryGetResolver : DefaultGetResolver<Category>() {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -43,20 +43,21 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
put(COL_LAST_PAGE_READ, obj.last_page_read)
put(COL_CHAPTER_NUMBER, obj.chapter_number)
put(COL_SOURCE_ORDER, obj.source_order)
}
override fun mapToContentValues(obj: Chapter) =
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_URL to obj.url,
COL_NAME to obj.name,
COL_READ to obj.read,
COL_SCANLATOR to obj.scanlator,
COL_BOOKMARK to obj.bookmark,
COL_DATE_FETCH to obj.date_fetch,
COL_DATE_UPLOAD to obj.date_upload,
COL_LAST_PAGE_READ to obj.last_page_read,
COL_CHAPTER_NUMBER to obj.chapter_number,
COL_SOURCE_ORDER to obj.source_order
)
}
class ChapterGetResolver : DefaultGetResolver<Chapter>() {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -35,12 +35,13 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_CHAPTER_ID, obj.chapter_id)
put(COL_LAST_READ, obj.last_read)
put(COL_TIME_READ, obj.time_read)
}
override fun mapToContentValues(obj: History) =
contentValuesOf(
COL_ID to obj.id,
COL_CHAPTER_ID to obj.chapter_id,
COL_LAST_READ to obj.last_read,
COL_TIME_READ to obj.time_read
)
}
class HistoryGetResolver : DefaultGetResolver<History>() {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -33,11 +33,12 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_CATEGORY_ID, obj.category_id)
}
override fun mapToContentValues(obj: MangaCategory) =
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_CATEGORY_ID to obj.category_id
)
}
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -48,25 +48,26 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist)
put(COL_AUTHOR, obj.author)
put(COL_DESCRIPTION, obj.description)
put(COL_GENRE, obj.genre)
put(COL_TITLE, obj.title)
put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
put(COL_LAST_UPDATE, obj.last_update)
put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
put(COL_DATE_ADDED, obj.date_added)
}
override fun mapToContentValues(obj: Manga) =
contentValuesOf(
COL_ID to obj.id,
COL_SOURCE to obj.source,
COL_URL to obj.url,
COL_ARTIST to obj.artist,
COL_AUTHOR to obj.author,
COL_DESCRIPTION to obj.description,
COL_GENRE to obj.genre,
COL_TITLE to obj.title,
COL_STATUS to obj.status,
COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update,
COL_INITIALIZED to obj.initialized,
COL_VIEWER to obj.viewer,
COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
COL_DATE_ADDED to obj.date_added
)
}
interface BaseMangaGetResolver {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
@ -44,21 +44,22 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id)
put(COL_MEDIA_ID, obj.media_id)
put(COL_LIBRARY_ID, obj.library_id)
put(COL_TITLE, obj.title)
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
put(COL_STATUS, obj.status)
put(COL_TRACKING_URL, obj.tracking_url)
put(COL_SCORE, obj.score)
put(COL_START_DATE, obj.started_reading_date)
put(COL_FINISH_DATE, obj.finished_reading_date)
}
override fun mapToContentValues(obj: Track) =
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_SYNC_ID to obj.sync_id,
COL_MEDIA_ID to obj.media_id,
COL_LIBRARY_ID to obj.library_id,
COL_TITLE to obj.title,
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
COL_TOTAL_CHAPTERS to obj.total_chapters,
COL_STATUS to obj.status,
COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date,
COL_FINISH_DATE to obj.finished_reading_date
)
}
class TrackGetResolver : DefaultGetResolver<Track>() {

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
@ -84,6 +85,11 @@ interface ChapterQueries : DbProvider {
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())

View File

@ -10,6 +10,15 @@ import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider {
fun getTracks() = db.get()
.listOfObjects(Track::class.java)
.withQuery(
Query.builder()
.table(TrackTable.TABLE)
.build()
)
.prepare()
fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java)
.withQuery(

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,9 +25,10 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
.whereArgs(chapter.url)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
)
}

View File

@ -0,0 +1,34 @@
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.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,9 +25,10 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
.whereArgs(chapter.url, chapter.manga_id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
}
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
)
}

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import androidx.annotation.NonNull
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query
@ -57,7 +57,8 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
* Create content query
* @param history object
*/
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read)
}
fun mapToUpdateContentValues(history: History) =
contentValuesOf(
HistoryTable.COL_LAST_READ to history.last_read
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_FAVORITE to manga.favorite
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -35,7 +35,8 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
}
}
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_LAST_UPDATE to manga.last_update
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_TITLE to manga.title
)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
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
@ -25,7 +25,8 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer)
}
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_VIEWER to manga.viewer
)
}

View File

@ -212,7 +212,7 @@ class DownloadManager(private val context: Context) {
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters)
queue.remove(filteredChapters)
removeFromDownloadQueue(filteredChapters)
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
@ -224,6 +224,24 @@ class DownloadManager(private val context: Context) {
return filteredChapters
}
private fun removeFromDownloadQueue(chapters: List<Chapter>) {
val wasRunning = downloader.isRunning
if (wasRunning) {
downloader.pause()
}
downloader.queue.remove(chapters)
if (wasRunning) {
if (downloader.queue.isEmpty()) {
DownloadService.stop(context)
downloader.stop()
} else if (downloader.queue.isNotEmpty()) {
downloader.start()
}
}
}
/**
* Deletes the directory of a downloaded manga.
*
@ -231,7 +249,7 @@ class DownloadManager(private val context: Context) {
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
queue.remove(manga)
downloader.queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
}

View File

@ -66,15 +66,6 @@ internal class DownloadNotifier(private val context: Context) {
context.notificationManager.notify(id, build())
}
/**
* Clear old actions if they exist.
*/
private fun NotificationCompat.Builder.clearActions() {
if (mActions.isNotEmpty()) {
mActions.clear()
}
}
/**
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user.
@ -165,6 +156,8 @@ internal class DownloadNotifier(private val context: Context) {
* This function shows a notification to inform download tasks are done.
*/
fun onComplete() {
dismissProgress()
if (!errorThrown) {
// Create notification
with(completeNotificationBuilder) {

View File

@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
val id: Long,
val url: String,
val name: String,
val scanlator: String?
val scanlator: String? = null
)
/**

View File

@ -9,11 +9,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.storage.DiskUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/**
@ -26,7 +25,7 @@ class DownloadProvider(private val context: Context) {
private val preferences: PreferencesHelper by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val scope = MainScope()
/**
* The root directory for downloads.
@ -55,6 +54,7 @@ class DownloadProvider(private val context: Context) {
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) {
Timber.w(e)
throw Exception(context.getString(R.string.invalid_download_dir))
}
}

View File

@ -16,8 +16,8 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
@ -114,8 +114,8 @@ class Downloader(
initializeSubscriptions()
}
val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
val pending = queue.filter { it.status != Download.State.DOWNLOADED }
pending.forEach { if (it.status != Download.State.QUEUE) it.status = Download.State.QUEUE }
notifier.paused = false
@ -129,20 +129,21 @@ class Downloader(
fun stop(reason: String? = null) {
destroySubscriptions()
queue
.filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.ERROR }
.filter { it.status == Download.State.DOWNLOADING }
.forEach { it.status = Download.State.ERROR }
if (reason != null) {
notifier.onWarning(reason)
} else {
if (notifier.paused) {
notifier.paused = false
notifier.onPaused()
} else {
notifier.dismissProgress()
notifier.onComplete()
}
return
}
if (notifier.paused && !queue.isEmpty()) {
notifier.onPaused()
} else {
notifier.onComplete()
}
notifier.paused = false
}
/**
@ -151,8 +152,8 @@ class Downloader(
fun pause() {
destroySubscriptions()
queue
.filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.QUEUE }
.filter { it.status == Download.State.DOWNLOADING }
.forEach { it.status = Download.State.QUEUE }
notifier.paused = true
}
@ -167,8 +168,8 @@ class Downloader(
// Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE }
.forEach { it.status = Download.NOT_DOWNLOADED }
.filter { it.status == Download.State.QUEUE }
.forEach { it.status = Download.State.NOT_DOWNLOADED }
}
queue.clear()
notifier.dismissProgress()
@ -227,8 +228,8 @@ class Downloader(
* @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async {
@ -271,7 +272,7 @@ class Downloader(
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.ERROR
download.status = Download.State.ERROR
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
return@defer Observable.just(download)
}
@ -301,7 +302,7 @@ class Downloader(
?.forEach { it.delete() }
download.downloadedImages = 0
download.status = Download.DOWNLOADING
download.status = Download.State.DOWNLOADING
}
// Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
@ -317,7 +318,7 @@ class Downloader(
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here
.onErrorReturn { error ->
download.status = Download.ERROR
download.status = Download.State.ERROR
notifier.onError(error.message, download.chapter.name)
download
}
@ -457,13 +458,13 @@ class Downloader(
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
download.status = if (downloadedImages.size == download.pages!!.size) {
Download.DOWNLOADED
Download.State.DOWNLOADED
} else {
Download.ERROR
Download.State.ERROR
}
// Only rename the directory if it's downloaded.
if (download.status == Download.DOWNLOADED) {
if (download.status == Download.State.DOWNLOADED) {
tmpDir.renameTo(dirname)
cache.addChapter(dirname, mangaDir, download.manga)
@ -476,7 +477,7 @@ class Downloader(
*/
private fun completeDownload(download: Download) {
// Delete successful downloads from queue
if (download.status == Download.DOWNLOADED) {
if (download.status == Download.State.DOWNLOADED) {
// remove downloaded chapter from queue
queue.remove(download)
}
@ -489,7 +490,7 @@ class Downloader(
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
*/
private fun areAllDownloadsFinished(): Boolean {
return queue.none { it.status <= Download.DOWNLOADING }
return queue.none { it.status.value <= Download.State.DOWNLOADING.value }
}
companion object {

View File

@ -20,7 +20,7 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
@Volatile
@Transient
var status: Int = 0
var status: State = State.NOT_DOWNLOADED
set(status) {
field = status
statusSubject?.onNext(this)
@ -47,11 +47,11 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
statusCallback = f
}
companion object {
const val NOT_DOWNLOADED = 0
const val QUEUE = 1
const val DOWNLOADING = 2
const val DOWNLOADED = 3
const val ERROR = 4
enum class State(val value: Int) {
NOT_DOWNLOADED(0),
QUEUE(1),
DOWNLOADING(2),
DOWNLOADED(3),
ERROR(4),
}
}

View File

@ -22,7 +22,7 @@ class DownloadQueue(
downloads.forEach { download ->
download.setStatusSubject(statusSubject)
download.setStatusCallback(::setPagesFor)
download.status = Download.QUEUE
download.status = Download.State.QUEUE
}
queue.addAll(downloads)
store.addAll(downloads)
@ -34,8 +34,8 @@ class DownloadQueue(
store.remove(download)
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
download.status = Download.State.NOT_DOWNLOADED
}
if (removed) {
updatedRelay.call(Unit)
@ -60,8 +60,8 @@ class DownloadQueue(
queue.forEach { download ->
download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
if (download.status == Download.State.DOWNLOADING || download.status == Download.State.QUEUE) {
download.status = Download.State.NOT_DOWNLOADED
}
}
queue.clear()
@ -70,7 +70,7 @@ class DownloadQueue(
}
fun getActiveDownloads(): Observable<Download> =
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
Observable.from(this).filter { download -> download.status == Download.State.DOWNLOADING }
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
@ -79,7 +79,7 @@ class DownloadQueue(
.map { this }
private fun setPagesFor(download: Download) {
if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
setPagesSubject(download.pages, null)
}
}
@ -88,19 +88,19 @@ class DownloadQueue(
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap { download ->
if (download.status == Download.DOWNLOADING) {
if (download.status == Download.State.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject
.onBackpressureBuffer()
.filter { it == Page.READY }
.map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
setPagesSubject(download.pages, null)
}
Observable.just(download)
}
.filter { it.status == Download.DOWNLOADING }
.filter { it.status == Download.State.DOWNLOADING }
}
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {

View File

@ -109,7 +109,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
errorLogIntent
)

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
@ -22,16 +23,27 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isServiceRunning
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -55,17 +67,11 @@ class LibraryUpdateService(
val coverCache: CoverCache = Injekt.get()
) : Service() {
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope
/**
* Subscription where the update is done.
*/
private var subscription: Subscription? = null
private var updateJob: Job? = null
/**
* Defines what should be updated within a service execution.
@ -140,6 +146,7 @@ class LibraryUpdateService(
override fun onCreate() {
super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = LibraryUpdateNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
@ -151,7 +158,8 @@ class LibraryUpdateService(
* lock.
*/
override fun onDestroy() {
subscription?.unsubscribe()
updateJob?.cancel()
ioScope?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -179,34 +187,25 @@ class LibraryUpdateService(
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
updateJob?.cancel()
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable
.defer {
val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
// Update either chapter list or manga details.
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
}
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
}
updateJob = ioScope.launch(handler) {
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
}
.subscribeOn(Schedulers.io())
.subscribe(
{
},
{
Timber.e(it)
stopSelf(startId)
},
{
stopSelf(startId)
}
)
}
updateJob?.invokeOnCompletion { stopSelf(startId) }
return START_REDELIVER_INTENT
}
@ -249,77 +248,57 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
// List containing new updates
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
val progressCount = AtomicInteger(0)
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
// List containing failed updates
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
// Boolean to determine if DownloadManager has downloads
var hasDownloads = false
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { notifier.showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga
.concatMap { manga ->
updateManga(manga)
// If there's any error, return empty update and continue.
.onErrorReturn {
val errorMessage = if (it is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
it.message
}
failedUpdates.add(Pair(manga, errorMessage))
Pair(emptyList(), emptyList())
}
// Filter out mangas without new chapters (or failed).
.filter { (first) -> first.isNotEmpty() }
.doOnNext {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, it.first)
hasDownloads = true
}
}
// Convert to the manga that contains new chapters.
.map {
Pair(
manga,
(
it.first.sortedByDescending { ch -> ch.source_order }
.toTypedArray()
)
)
}
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
return
}
// Add manga with new chapters to the list.
.doOnNext { manga ->
// Add to the list
newUpdates.add(manga)
}
// Notify result of the overall update.
.doOnCompleted {
notifier.cancelProgressNotification()
if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates)
if (hasDownloads) {
DownloadService.start(this)
}
}
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title },
errorFile.getUriCompat(this)
)
try {
val (newChapters, _) = updateManga(manga)
if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads = true
}
// Convert to the manga that contains new chapters
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
}
} catch (e: Throwable) {
val errorMessage = if (e is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
e.message
}
failedUpdates.add(manga to errorMessage)
}
.map { (first) -> first }
}
notifier.cancelProgressNotification()
if (newUpdates.isNotEmpty()) {
notifier.showUpdateNotifications(newUpdates)
if (hasDownloads) {
DownloadService.start(this)
}
}
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title },
errorFile.getUriCompat(this)
)
}
}
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -334,94 +313,102 @@ class LibraryUpdateService(
* @param manga the manga to update.
* @return a pair of the inserted and removed chapters.
*/
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
val source = sourceManager.getOrStub(manga.source)
// Update manga details metadata in the background
if (preferences.autoUpdateMetadata()) {
source.fetchMangaDetails(manga)
.map { updatedManga ->
// Avoid "losing" existing cover
if (!updatedManga.thumbnail_url.isNullOrEmpty()) {
manga.prepUpdateCover(coverCache, updatedManga, false)
} else {
updatedManga.thumbnail_url = manga.thumbnail_url
}
manga.copyFrom(updatedManga)
db.insertManga(manga).executeAsBlocking()
manga
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
}
GlobalScope.launch(Dispatchers.IO + handler) {
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
val sManga = updatedManga.toSManga()
// Avoid "losing" existing cover
if (!sManga.thumbnail_url.isNullOrEmpty()) {
manga.prepUpdateCover(coverCache, sManga, false)
} else {
sManga.thumbnail_url = manga.thumbnail_url
}
.onErrorResumeNext { Observable.just(manga) }
.subscribeOn(Schedulers.io())
.subscribe()
manga.copyFrom(sManga)
db.insertManga(manga).executeAsBlocking()
}
}
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
return syncChaptersWithSource(db, chapters, manga, source)
}
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
var count = 0
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
var progressCount = 0
return Observable.from(mangaToUpdate)
.doOnNext {
notifier.showProgressNotification(it, count++, mangaToUpdate.size)
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
return
}
.flatMap { manga ->
val source = sourceManager.get(manga.source)
?: return@flatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
manga.prepUpdateCover(coverCache, networkManga, true)
networkManga.thumbnail_url?.let {
manga.thumbnail_url = it
db.insertManga(manga).executeAsBlocking()
}
manga
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
sourceManager.get(manga.source)?.let { source ->
try {
val networkManga = source.getMangaDetails(manga.toMangaInfo())
val sManga = networkManga.toSManga()
manga.prepUpdateCover(coverCache, sManga, true)
sManga.thumbnail_url?.let {
manga.thumbnail_url = it
db.insertManga(manga).executeAsBlocking()
}
.onErrorReturn { manga }
}
.doOnCompleted {
notifier.cancelProgressNotification()
} catch (e: Throwable) {
// Ignore errors and continue
Timber.e(e)
}
}
}
notifier.cancelProgressNotification()
}
/**
* 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.
*/
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
var count = 0
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged }
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { notifier.showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details.
.concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking()
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
return
}
Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track }
} else {
Observable.empty()
// Notify manga that will update.
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
// Update the tracking details.
db.getTracks(manga).executeAsBlocking()
.map { track ->
supervisorScope {
async {
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
try {
val updatedTrack = service.refresh(track)
db.insertTrack(updatedTrack).executeAsBlocking()
} catch (e: Throwable) {
// Ignore errors and continue
Timber.e(e)
}
}
}
}
.map { manga }
}
.doOnCompleted {
notifier.cancelProgressNotification()
}
}
.awaitAll()
}
notifier.cancelProgressNotification()
}
/**
@ -430,15 +417,14 @@ class LibraryUpdateService(
private fun writeErrorFile(errors: List<Pair<Manga, String?>>): File {
try {
if (errors.isNotEmpty()) {
val destFile = File(externalCacheDir, "tachiyomi_update_errors.txt")
destFile.bufferedWriter().use { out ->
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
file.bufferedWriter().use { out ->
errors.forEach { (manga, error) ->
val source = sourceManager.getOrStub(manga.source)
out.write("${manga.title} ($source): $error\n")
}
}
return destFile
return file
}
} catch (e: Exception) {
// Empty

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -69,9 +70,10 @@ class NotificationReceiver : BroadcastReceiver() {
)
// Share backup file
ACTION_SHARE_BACKUP ->
shareBackup(
shareFile(
context,
intent.getParcelableExtra(EXTRA_URI),
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
ACTION_CANCEL_RESTORE -> cancelRestore(
@ -100,6 +102,14 @@ class NotificationReceiver : BroadcastReceiver() {
markAsRead(urls, mangaId)
}
}
// Share crash dump file
ACTION_SHARE_CRASH_LOG ->
shareFile(
context,
intent.getParcelableExtra(EXTRA_URI),
"text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
}
}
@ -120,14 +130,13 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
*/
private fun shareImage(context: Context, path: String, notificationId: Int) {
// Create intent
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = "image/*"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
dismissNotification(context, notificationId)
// Launch share activity
context.startActivity(intent)
@ -140,10 +149,11 @@ class NotificationReceiver : BroadcastReceiver() {
* @param path path of file
* @param notificationId id of notification
*/
private fun shareBackup(context: Context, uri: Uri, notificationId: Int) {
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/json"
clipData = ClipData.newRawUri(null, uri)
type = fileMimeType
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
@ -244,59 +254,34 @@ class NotificationReceiver : BroadcastReceiver() {
companion object {
private const val NAME = "NotificationReceiver"
// Called to launch share intent.
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
// Called to delete image.
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
// Called to launch send intent.
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
// Called to cancel backup restore job.
private const val ACTION_SHARE_CRASH_LOG = "$ID.$NAME.SEND_CRASH_LOG"
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
// Called to cancel library update.
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
// Called to mark manga chapters as read.
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
// Called to open chapter.
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
// Value containing file location.
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
// Called to resume downloads.
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
// Called to pause downloads.
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
// Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
// Value containing uri.
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
private const val EXTRA_URI = "$ID.$NAME.URI"
// Value containing notification id.
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
// Value containing group id.
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
// Value containing manga id.
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
// Value containing chapter id.
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
// Value containing chapter url.
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
/**
* Returns a [PendingIntent] that resumes the download of a chapter
@ -509,10 +494,11 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_BACKUP
putExtra(EXTRA_URI, uri)
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@ -534,6 +520,23 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getActivity(context, 0, intent, 0)
}
/**
* Returns [PendingIntent] that starts a share activity for a crash log dump file.
*
* @param context context of application
* @param uri uri of file
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun shareCrashLogPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_CRASH_LOG
putExtra(EXTRA_URI, uri)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Returns [PendingIntent] that cancels a backup restore job.
*

View File

@ -62,6 +62,12 @@ object Notifications {
const val ID_BACKUP_COMPLETE = -502
const val ID_RESTORE_COMPLETE = -504
/**
* Notification channel used for crash log file sharing.
*/
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
const val ID_CRASH_LOGS = -601
private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel"
@ -143,7 +149,12 @@ object Notifications {
group = GROUP_BACKUP_RESTORE
setShowBadge(false)
setSound(null, null)
}
},
NotificationChannel(
CHANNEL_CRASH_LOGS,
context.getString(R.string.channel_crash_logs),
NotificationManager.IMPORTANCE_HIGH
)
).forEach(context.notificationManager::createNotificationChannel)
// Delete old notification channels

View File

@ -23,6 +23,8 @@ object PreferenceKeys {
const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplit = "pref_dual_page_split"
const val showReadingMode = "pref_show_reading_mode"
const val trueColor = "pref_true_color_key"
@ -57,7 +59,9 @@ object PreferenceKeys {
const val readWithTapping = "reader_tap"
const val readWithTappingInverted = "reader_tapping_inverted"
const val pagerNavInverted = "reader_tapping_inverted"
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
const val readWithLongTap = "reader_long_tap"
@ -65,6 +69,10 @@ object PreferenceKeys {
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
const val navigationModePager = "reader_navigation_mode_pager"
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"
@ -117,11 +125,15 @@ object PreferenceKeys {
const val filterCompleted = "pref_filter_library_completed"
const val filterTracked = "pref_filter_library_tracked"
const val librarySortingMode = "library_sorting_mode"
const val automaticExtUpdates = "automatic_ext_updates"
const val allowNsfwSource = "allow_nsfw_source"
const val showNsfwSource = "show_nsfw_source"
const val showNsfwExtension = "show_nsfw_extension"
const val labelNsfwExtension = "label_nsfw_extension"
const val startScreen = "start_screen"
@ -161,6 +173,8 @@ object PreferenceKeys {
const val categoryTabs = "display_category_tabs"
const val categoryNumberOfItems = "display_number_of_items"
const val alwaysShowChapterTransition = "always_show_chapter_transition"
const val searchPinnedSourcesOnly = "search_pinned_sources_only"

View File

@ -31,16 +31,10 @@ object PreferenceValues {
LIST,
}
enum class TappingInvertMode {
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
enum class NsfwAllowance {
ALLOWED,
PARTIAL,
BLOCKED
HORIZONTAL(shouldInvertHorizontal = true),
VERTICAL(shouldInvertVertical = true),
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
}
}

View File

@ -10,11 +10,9 @@ import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferenceValues.NsfwAllowance
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import java.io.File
@ -24,7 +22,6 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
block(get())
return asFlow()
@ -39,7 +36,6 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
@OptIn(ExperimentalCoroutinesApi::class)
class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -93,6 +89,8 @@ class PreferencesHelper(val context: Context) {
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
@ -131,7 +129,9 @@ class PreferencesHelper(val context: Context) {
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
fun pagerNavInverted() = flowPrefs.getEnum(Keys.pagerNavInverted, Values.TappingInvertMode.NONE)
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, Values.TappingInvertMode.NONE)
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
@ -139,6 +139,10 @@ class PreferencesHelper(val context: Context) {
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
@ -213,19 +217,25 @@ class PreferencesHelper(val context: Context) {
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun allowNsfwSource() = flowPrefs.getEnum(Keys.allowNsfwSource, NsfwAllowance.ALLOWED)
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)

View File

@ -1,14 +1,14 @@
package eu.kanade.tachiyomi.data.track
import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class TrackService(val id: Int) {
@ -20,7 +20,8 @@ abstract class TrackService(val id: Int) {
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
@StringRes
abstract fun nameRes(): Int
// Application and remote support for reading dates
open val supportsReadingDates: Boolean = false
@ -28,6 +29,7 @@ abstract class TrackService(val id: Int) {
@DrawableRes
abstract fun getLogo(): Int
@ColorInt
abstract fun getLogoColor(): Int
abstract fun getStatusList(): List<Int>
@ -44,17 +46,17 @@ abstract class TrackService(val id: Int) {
abstract fun displayScore(track: Track): String
abstract fun add(track: Track): Observable<Track>
abstract suspend fun add(track: Track): Track
abstract fun update(track: Track): Observable<Track>
abstract suspend fun update(track: Track): Track
abstract fun bind(track: Track): Observable<Track>
abstract suspend fun bind(track: Track): Track
abstract fun search(query: String): Observable<List<TrackSearch>>
abstract suspend fun search(query: String): List<TrackSearch>
abstract fun refresh(track: Track): Observable<Track>
abstract suspend fun refresh(track: Track): Track
abstract fun login(username: String, password: String): Completable
abstract suspend fun login(username: String, password: String)
@CallSuper
open fun logout() {

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
@ -9,8 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class Anilist(private val context: Context, id: Int) : TrackService(id) {
@ -23,9 +22,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val POINT_100 = "POINT_100"
const val POINT_10 = "POINT_10"
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
@ -33,8 +29,6 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
const val POINT_3 = "POINT_3"
}
override val name = "AniList"
private val json: Json by injectLazy()
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
@ -53,6 +47,9 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
}
@StringRes
override fun nameRes() = R.string.tracker_anilist
override fun getLogo() = R.drawable.ic_tracker_anilist
override fun getLogoColor() = Color.rgb(18, 25, 35)
@ -131,65 +128,58 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
}
override fun add(track: Track): Observable<Track> {
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
override suspend fun update(track: Track): Track {
// If user was using API v1 fetch library_id
if (track.library_id == null || track.library_id!! == 0L) {
return api.findLibManga(track, getUsername().toInt()).flatMap {
if (it == null) {
throw Exception("$track not found on user library")
}
track.library_id = it.library_id
api.updateLibManga(track)
}
val libManga = api.findLibManga(track, getUsername().toInt())
?: throw Exception("$track not found on user library")
track.library_id = libManga.library_id
}
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername().toInt())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.status = READING
track.score = 0F
add(track)
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track, getUsername().toInt())
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track, getUsername().toInt())
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
return track
}
override fun login(username: String, password: String) = login(password)
override suspend fun login(username: String, password: String) = login(password)
fun login(token: String): Completable {
val oauth = api.createOAuth(token)
interceptor.setAuth(oauth)
return api.getCurrentUser().map { (username, scoreType) ->
suspend fun login(token: String) {
try {
val oauth = api.createOAuth(token)
interceptor.setAuth(oauth)
val (username, scoreType) = api.getCurrentUser()
scorePreference.set(scoreType)
saveCredentials(username.toString(), oauth.access_token)
}.doOnError {
} catch (e: Throwable) {
logout()
}.toCompletable()
}
}
override fun logout() {

View File

@ -4,9 +4,11 @@ import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
@ -18,24 +20,18 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val json: Json by injectLazy()
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
val query =
"""
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val query =
"""
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
@ -43,36 +39,34 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", track.media_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
netResponse.close()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", track.media_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
}
val response = json.decodeFromString<JsonObject>(responseBody)
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
}
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
)
.await()
.parseAs<JsonObject>()
.let {
track.library_id =
it["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
}
}
}
fun updateLibManga(track: Track): Observable<Track> {
val query =
"""
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
val query =
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|id
@ -81,30 +75,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("listId", track.library_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
put("score", track.score.toInt())
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("listId", track.library_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
put("score", track.score.toInt())
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
.await()
track
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}
}
fun search(search: String): Observable<List<TrackSearch>> {
val query =
"""
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val query =
"""
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -128,36 +117,34 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("query", search)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("query", search)
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
}
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
)
.await()
.parseAs<JsonObject>()
.let { response ->
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
}
}
}
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
val query =
"""
suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext {
val query =
"""
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -187,46 +174,43 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("id", userid)
put("manga_id", track.media_id)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("id", userid)
put("manga_id", track.media_id)
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
}
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
)
.await()
.parseAs<JsonObject>()
.let { response ->
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
}
}
}
fun getLibManga(track: Track, userid: Int): Observable<Track> {
return findLibManga(track, userid)
.map { it ?: throw Exception("Could not find manga") }
suspend fun getLibManga(track: Track, userid: Int): Track {
return findLibManga(track, userid) ?: throw Exception("Could not find manga")
}
fun createOAuth(token: String): OAuth {
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
}
fun getCurrentUser(): Observable<Pair<Int, String>> {
val query =
"""
suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext {
val query =
"""
|query User {
|Viewer {
|id
@ -236,26 +220,26 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = buildJsonObject {
put("query", query)
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val viewer = data["Viewer"]!!.jsonObject
Pair(viewer["id"]!!.jsonPrimitive.int, viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content)
val payload = buildJsonObject {
put("query", query)
}
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
)
.await()
.parseAs<JsonObject>()
.let {
val data = it["data"]!!.jsonObject
val viewer = data["Viewer"]!!.jsonObject
Pair(
viewer["id"]!!.jsonPrimitive.int,
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
)
}
}
}
private fun jsonToALManga(struct: JsonObject): ALManga {
@ -298,7 +282,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
companion object {
private const val clientId = "385"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val apiUrl = "https://graphql.anilist.co/"
private const val baseUrl = "https://anilist.co/api/v2/"
private const val baseMangaUrl = "https://anilist.co/manga/"

View File

@ -63,7 +63,7 @@ data class ALUserManga(
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING
"REPEATING" -> Anilist.REPEATING
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $list_status")
}
}
@ -74,7 +74,7 @@ fun Track.toAnilistStatus() = when (status) {
Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: PreferencesHelper by injectLazy()
@ -102,5 +102,5 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get())
}
// 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw Exception("Unknown score type")
else -> throw NotImplementedError("Unknown score type")
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.bangumi
import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
@ -9,20 +10,19 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override val name = "Bangumi"
private val json: Json by injectLazy()
private val interceptor by lazy { BangumiInterceptor(this) }
private val api by lazy { BangumiApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_bangumi
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
@ -31,52 +31,44 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString()
}
override fun add(track: Track): Observable<Track> {
override suspend fun add(track: Track): Track {
return api.addLibManga(track)
}
override fun update(track: Track): Observable<Track> {
override suspend fun update(track: Track): Track {
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.statusLibManga(track)
.flatMap {
api.findLibManga(track).flatMap { remoteTrack ->
if (remoteTrack != null && it != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read
refresh(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
update(track)
}
}
}
override suspend fun bind(track: Track): Track {
val statusTrack = api.statusLibManga(track)
val remoteTrack = api.findLibManga(track)
return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read
refresh(track)
} else {
// Set default fields if it's not found in the list
track.status = READING
track.score = 0F
add(track)
update(track)
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.statusLibManga(track)
.flatMap {
track.copyPersonalFrom(it!!)
api.findLibManga(track)
.map { remoteTrack ->
if (remoteTrack != null) {
track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
}
track
}
}
override suspend fun refresh(track: Track): Track {
val remoteStatusTrack = api.statusLibManga(track)
track.copyPersonalFrom(remoteStatusTrack!!)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
}
return track
}
override fun getLogo() = R.drawable.ic_tracker_bangumi
@ -100,17 +92,16 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun getCompletionStatus(): Int = COMPLETED
override fun login(username: String, password: String) = login(password)
override suspend fun login(username: String, password: String) = login(password)
fun login(code: String): Completable {
return api.accessToken(code).map { oauth: OAuth? ->
suspend fun login(code: String) {
try {
val oauth = api.accessToken(code)
interceptor.newAuth(oauth)
if (oauth != null) {
saveCredentials(oauth.user_id.toString(), oauth.access_token)
}
}.doOnError {
saveCredentials(oauth.user_id.toString(), oauth.access_token)
} catch (e: Throwable) {
logout()
}.toCompletable()
}
}
fun saveToken(oauth: OAuth?) {
@ -137,8 +128,5 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
const val ON_HOLD = 4
const val DROPPED = 5
const val PLANNING = 1
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
}

View File

@ -5,12 +5,14 @@ import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -19,7 +21,6 @@ import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
@ -29,76 +30,64 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
val body = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
val request = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val body = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = body))
.await()
track
}
}
fun updateLibManga(track: Track): Observable<Track> {
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString())
.build()
val request = Request.Builder()
.url("$apiUrl/subject/${track.media_id}/update/watched_eps")
.post(body)
.build()
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
// read status update
val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus())
.build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
.await()
// read status update
val sbody = FormBody.Builder()
.add("status", track.toBangumiStatus())
.build()
val srequest = Request.Builder()
.url("$apiUrl/collection/${track.media_id}/update")
.post(sbody)
.build()
return authClient.newCall(srequest)
.asObservableSuccess()
.map {
track
}.flatMap {
authClient.newCall(request)
.asObservableSuccess()
.map {
track
// chapter update
val body = FormBody.Builder()
.add("watched_eps", track.last_chapter_read.toString())
.build()
authClient.newCall(
POST(
"$apiUrl/subject/${track.media_id}/update/watched_eps",
body = body
)
).await()
track
}
}
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
.toUri()
.buildUpon()
.appendQueryParameter("max_results", "20")
.build()
authClient.newCall(GET(url.toString()))
.await()
.use {
var responseBody = it.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
}
}
fun search(search: String): Observable<List<TrackSearch>> {
val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
.toUri()
.buildUpon()
.appendQueryParameter("max_results", "20")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
var responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
if (responseBody.contains("\"code\":404")) {
responseBody = "{\"results\":0,\"list\":[]}"
}
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }
?.map { jsonToSearch(it.jsonObject) }.orEmpty()
}
if (responseBody.contains("\"code\":404")) {
responseBody = "{\"results\":0,\"list\":[]}"
}
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }?.map { jsonToSearch(it.jsonObject) }
}
}
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
@ -111,63 +100,41 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
}
private fun jsonToTrack(mangas: JsonObject): Track {
return Track.create(TrackManager.BANGUMI).apply {
title = mangas["name"]!!.jsonPrimitive.content
media_id = mangas["id"]!!.jsonPrimitive.int
score = try {
mangas["rating"]!!.jsonObject["score"]!!.jsonPrimitive.float
} catch (_: Exception) {
0f
}
status = Bangumi.DEFAULT_STATUS
tracking_url = mangas["url"]!!.jsonPrimitive.content
suspend fun findLibManga(track: Track): Track? {
return withIOContext {
authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
.await()
.parseAs<JsonObject>()
.let { jsonToSearch(it) }
}
}
fun findLibManga(track: Track): Observable<Track?> {
val urlMangas = "$apiUrl/subject/${track.media_id}"
val requestMangas = Request.Builder()
.url(urlMangas)
.get()
.build()
suspend fun statusLibManga(track: Track): Track? {
return withIOContext {
val urlUserRead = "$apiUrl/collection/${track.media_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build()
return authClient.newCall(requestMangas)
.asObservableSuccess()
.map { netResponse ->
// get comic info
val responseBody = netResponse.body?.string().orEmpty()
jsonToTrack(json.decodeFromString(responseBody))
}
// TODO: get user readed chapter here
authClient.newCall(requestUserRead)
.await()
.parseAs<Collection>()
.let {
track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!!
track
}
}
}
fun statusLibManga(track: Track): Observable<Track?> {
val urlUserRead = "$apiUrl/collection/${track.media_id}"
val requestUserRead = Request.Builder()
.url(urlUserRead)
.cacheControl(CacheControl.FORCE_NETWORK)
.get()
.build()
// todo get user readed chapter here
return authClient.newCall(requestUserRead)
.asObservableSuccess()
.map { netResponse ->
val resp = netResponse.body?.string()
val coll = json.decodeFromString<Collection>(resp!!)
track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!!
track
}
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
json.decodeFromString<OAuth>(responseBody)
suspend fun accessToken(code: String): OAuth {
return withIOContext {
client.newCall(accessTokenRequest(code))
.await()
.parseAs()
}
}

View File

@ -8,7 +8,7 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
@ -17,6 +17,5 @@ fun toTrackStatus(status: String) = when (status) {
"on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING
else -> throw Exception("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}

View File

@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.data.track.kitsu
import android.content.Context
import android.graphics.Color
import com.google.gson.Gson
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import rx.Completable
import rx.Observable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
@ -20,16 +21,14 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0f
}
override val name = "Kitsu"
@StringRes
override fun nameRes() = R.string.tracker_kitsu
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
private val interceptor by lazy { KitsuInterceptor(this, gson) }
private val interceptor by lazy { KitsuInterceptor(this) }
private val api by lazy { KitsuApi(client, interceptor) }
@ -68,49 +67,43 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
return df.format(track.score)
}
override fun add(track: Track): Observable<Track> {
override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUserId())
}
override fun update(track: Track): Observable<Track> {
override suspend fun update(track: Track): Track {
return api.updateLibManga(track)
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUserId())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.media_id = remoteTrack.media_id
update(track)
} else {
track.score = DEFAULT_SCORE
track.status = DEFAULT_STATUS
add(track)
}
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUserId())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.media_id = remoteTrack.media_id
update(track)
} else {
track.status = READING
track.score = 0F
add(track)
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track)
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
override suspend fun refresh(track: Track): Track {
val remoteTrack = api.getLibManga(track)
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
return track
}
override fun login(username: String, password: String): Completable {
return api.login(username, password)
.doOnNext { interceptor.newAuth(it) }
.flatMap { api.getCurrentUser() }
.doOnNext { userId -> saveCredentials(username, userId) }
.doOnError { logout() }
.toCompletable()
override suspend fun login(username: String, password: String) {
val token = api.login(username, password)
interceptor.newAuth(token)
val userId = api.getCurrentUser()
saveCredentials(username, userId)
}
override fun logout() {
@ -123,13 +116,12 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
}
fun saveToken(oauth: OAuth?) {
val json = gson.toJson(oauth)
preferences.trackToken(this).set(json)
preferences.trackToken(this).set(json.encodeToString(oauth))
}
fun restoreToken(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
} catch (e: Exception) {
null
}

View File

@ -1,254 +1,253 @@
package eu.kanade.tachiyomi.data.track.kitsu
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.FormBody
import okhttp3.Headers.Companion.headersOf
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
import rx.Observable
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
private val rest = Retrofit.Builder()
.baseUrl(baseUrl)
.client(authClient)
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().serializeNulls().create()))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(Rest::class.java)
suspend fun addLibManga(track: Track, userId: String): Track {
return withIOContext {
val data = buildJsonObject {
putJsonObject("data") {
put("type", "libraryEntries")
putJsonObject("attributes") {
put("status", track.toKitsuStatus())
put("progress", track.last_chapter_read)
}
putJsonObject("relationships") {
putJsonObject("user") {
putJsonObject("data") {
put("id", userId)
put("type", "users")
}
}
putJsonObject("media") {
putJsonObject("data") {
put("id", track.media_id)
put("type", "manga")
}
}
}
}
}
private val searchRest = Retrofit.Builder()
.baseUrl(algoliaKeyUrl)
.client(authClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(SearchKeyRest::class.java)
private val algoliaRest = Retrofit.Builder()
.baseUrl(algoliaUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(AgoliaSearchRest::class.java)
fun addLibManga(track: Track, userId: String): Observable<Track> {
return Observable.defer {
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"attributes" to jsonObject(
"status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read
),
"relationships" to jsonObject(
"user" to jsonObject(
"data" to jsonObject(
"id" to userId,
"type" to "users"
)
authClient.newCall(
POST(
"${baseUrl}library-entries",
headers = headersOf(
"Content-Type",
"application/vnd.api+json"
),
"media" to jsonObject(
"data" to jsonObject(
"id" to track.media_id,
"type" to "manga"
)
)
body = data.toString().toRequestBody("application/vnd.api+json".toMediaType())
)
)
rest.addLibManga(jsonObject("data" to data))
.map { json ->
track.media_id = json["data"]["id"].int
.await()
.parseAs<JsonObject>()
.let {
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int
track
}
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
// @formatter:off
val data = jsonObject(
"type" to "libraryEntries",
"id" to track.media_id,
"attributes" to jsonObject(
"status" to track.toKitsuStatus(),
"progress" to track.last_chapter_read,
"ratingTwenty" to track.toKitsuScore()
)
)
// @formatter:on
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
val data = buildJsonObject {
putJsonObject("data") {
put("type", "libraryEntries")
put("id", track.media_id)
putJsonObject("attributes") {
put("status", track.toKitsuStatus())
put("progress", track.last_chapter_read)
put("ratingTwenty", track.toKitsuScore())
}
}
}
rest.updateLibManga(track.media_id, jsonObject("data" to data))
.map { track }
authClient.newCall(
Request.Builder()
.url("${baseUrl}library-entries/${track.media_id}")
.headers(
headersOf(
"Content-Type",
"application/vnd.api+json"
)
)
.patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType()))
.build()
)
.await()
.parseAs<JsonObject>()
.let {
track
}
}
}
fun search(query: String): Observable<List<TrackSearch>> {
return searchRest
.getKey().map { json ->
json["media"].asJsonObject["key"].string
}.flatMap { key ->
algoliaSearch(key, query)
}
}
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
return algoliaRest
.getSearchQuery(algoliaAppId, key, jsonObject)
.map { json ->
val data = json["hits"].array
data.map { KitsuSearchManga(it.obj) }
.filter { it.subType != "novel" }
.map { it.toTrack() }
}
}
fun findLibManga(track: Track, userId: String): Observable<Track?> {
return rest.findLibManga(track.media_id, userId)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
val manga = json["included"].array[0].obj
KitsuLibManga(data[0].obj, manga).toTrack()
} else {
null
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
authClient.newCall(GET(algoliaKeyUrl))
.await()
.parseAs<JsonObject>()
.let {
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
algoliaSearch(key, query)
}
}
}
}
fun getLibManga(track: Track): Observable<Track> {
return rest.getLibManga(track.media_id)
.map { json ->
val data = json["data"].array
if (data.size() > 0) {
val manga = json["included"].array[0].obj
KitsuLibManga(data[0].obj, manga).toTrack()
} else {
throw Exception("Could not find manga")
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
return withIOContext {
val jsonObject = buildJsonObject {
put("params", "query=$query$algoliaFilter")
}
client.newCall(
POST(
algoliaUrl,
headers = headersOf(
"X-Algolia-Application-Id",
algoliaAppId,
"X-Algolia-API-Key",
key,
),
body = jsonObject.toString().toRequestBody(jsonMime)
)
)
.await()
.parseAs<JsonObject>()
.let {
it["hits"]!!.jsonArray
.map { KitsuSearchManga(it.jsonObject) }
.filter { it.subType != "novel" }
.map { it.toTrack() }
}
}
}
}
fun login(username: String, password: String): Observable<OAuth> {
return Retrofit.Builder()
.baseUrl(loginUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(LoginRest::class.java)
.requestAccessToken(username, password)
suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon()
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonObject>()
.let {
val data = it["data"]!!.jsonArray
if (data.size > 0) {
val manga = it["included"]!!.jsonArray[0].jsonObject
KitsuLibManga(data[0].jsonObject, manga).toTrack()
} else {
null
}
}
}
}
fun getCurrentUser(): Observable<String> {
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
suspend fun getLibManga(track: Track): Track {
return withIOContext {
val url = "${baseUrl}library-entries".toUri().buildUpon()
.encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonObject>()
.let {
val data = it["data"]!!.jsonArray
if (data.size > 0) {
val manga = it["included"]!!.jsonArray[0].jsonObject
KitsuLibManga(data[0].jsonObject, manga).toTrack()
} else {
throw Exception("Could not find manga")
}
}
}
}
private interface Rest {
@Headers("Content-Type: application/vnd.api+json")
@POST("library-entries")
fun addLibManga(
@Body data: JsonObject
): Observable<JsonObject>
@Headers("Content-Type: application/vnd.api+json")
@PATCH("library-entries/{id}")
fun updateLibManga(
@Path("id") remoteId: Int,
@Body data: JsonObject
): Observable<JsonObject>
@GET("library-entries")
fun findLibManga(
@Query("filter[manga_id]", encoded = true) remoteId: Int,
@Query("filter[user_id]", encoded = true) userId: String,
@Query("include") includes: String = "manga"
): Observable<JsonObject>
@GET("library-entries")
fun getLibManga(
@Query("filter[id]", encoded = true) remoteId: Int,
@Query("include") includes: String = "manga"
): Observable<JsonObject>
@GET("users")
fun getCurrentUser(
@Query("filter[self]", encoded = true) self: Boolean = true
): Observable<JsonObject>
suspend fun login(username: String, password: String): OAuth {
return withIOContext {
val formBody: RequestBody = FormBody.Builder()
.add("username", username)
.add("password", password)
.add("grant_type", "password")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.build()
client.newCall(POST(loginUrl, body = formBody))
.await()
.parseAs()
}
}
private interface SearchKeyRest {
@GET("media/")
fun getKey(): Observable<JsonObject>
}
private interface AgoliaSearchRest {
@POST("query/")
fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
}
private interface LoginRest {
@FormUrlEncoded
@POST("oauth/token")
fun requestAccessToken(
@Field("username") username: String,
@Field("password") password: String,
@Field("grant_type") grantType: String = "password",
@Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret
): Observable<OAuth>
suspend fun getCurrentUser(): String {
return withIOContext {
val url = "${baseUrl}users".toUri().buildUpon()
.encodedQuery("filter[self]=true")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonObject>()
.let {
it["data"]!!.jsonArray[0].jsonObject["id"]!!.jsonPrimitive.content
}
}
}
companion object {
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val clientId =
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
private const val clientSecret =
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
private const val baseUrl = "https://kitsu.io/api/edge/"
private const val loginUrl = "https://kitsu.io/api/"
private const val loginUrl = "https://kitsu.io/api/oauth/token"
private const val baseMangaUrl = "https://kitsu.io/manga/"
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/media/"
private const val algoliaUrl =
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/"
private const val algoliaAppId = "AWQO5J657S"
private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
private const val algoliaFilter =
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
fun mangaUrl(remoteId: Int): String {
return baseMangaUrl + remoteId
}
fun refreshTokenRequest(token: String) = POST(
"${loginUrl}oauth/token",
loginUrl,
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", token)
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build()
)
}

View File

@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.data.track.kitsu
import com.google.gson.Gson
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
class KitsuInterceptor(val kitsu: Kitsu) : Interceptor {
private val json: Json by injectLazy()
/**
* OAuth object used for authenticated requests.
@ -22,7 +26,7 @@ class KitsuInterceptor(val kitsu: Kitsu, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) {
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
newAuth(json.decodeFromString(response.body!!.string()))
} else {
response.close()
}

View File

@ -1,32 +1,36 @@
package eu.kanade.tachiyomi.data.track.kitsu
import androidx.annotation.CallSuper
import com.github.salomonbrys.kotson.byInt
import com.github.salomonbrys.kotson.byString
import com.github.salomonbrys.kotson.nullInt
import com.github.salomonbrys.kotson.nullObj
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class KitsuSearchManga(obj: JsonObject) {
val id by obj.byInt
private val canonicalTitle by obj.byString
private val chapterCount = obj.get("chapterCount").nullInt
val subType = obj.get("subtype").nullString
val original = obj.get("posterImage").nullObj?.get("original")?.asString
private val synopsis by obj.byString
private var startDate = obj.get("startDate").nullString?.let {
val id = obj["id"]!!.jsonPrimitive.int
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
val original = try {
obj["posterImage"]?.jsonObject?.get("original")?.jsonPrimitive?.content
} catch (e: IllegalArgumentException) {
// posterImage is sometimes a jsonNull object instead
null
}
private val synopsis = obj["synopsis"]!!.jsonPrimitive.content
private var startDate = obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(Date(it.toLong() * 1000))
}
private val endDate = obj.get("endDate").nullString
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
@CallSuper
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
@ -47,17 +51,17 @@ class KitsuSearchManga(obj: JsonObject) {
}
class KitsuLibManga(obj: JsonObject, manga: JsonObject) {
val id by manga.byInt
private val canonicalTitle by manga["attributes"].byString
private val chapterCount = manga["attributes"].obj.get("chapterCount").nullInt
val type = manga["attributes"].obj.get("mangaType").nullString.orEmpty()
val original by manga["attributes"].obj["posterImage"].byString
private val synopsis by manga["attributes"].byString
private val startDate = manga["attributes"].obj.get("startDate").nullString.orEmpty()
private val libraryId by obj.byInt("id")
val status by obj["attributes"].byString
private val ratingTwenty = obj["attributes"].obj.get("ratingTwenty").nullString
val progress by obj["attributes"].byInt
val id = manga["id"]!!.jsonPrimitive.int
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull
val type = manga["attributes"]!!.jsonObject["mangaType"]?.jsonPrimitive?.contentOrNull.orEmpty()
val original = manga["attributes"]!!.jsonObject["posterImage"]!!.jsonObject["original"]!!.jsonPrimitive.content
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty()
private val libraryId = obj["id"]!!.jsonPrimitive.int
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
fun toTrack() = TrackSearch.create(TrackManager.KITSU).apply {
media_id = libraryId

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.kitsu
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,

View File

@ -2,13 +2,15 @@ package eu.kanade.tachiyomi.data.track.myanimelist
import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import rx.Completable
import rx.Observable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
@ -18,20 +20,19 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 6
const val REREADING = 7
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
const val BASE_URL = "https://myanimelist.net"
const val USER_SESSION_COOKIE = "MALSESSIONID"
const val LOGGED_IN_COOKIE = "is_logged_in"
private const val SEARCH_ID_PREFIX = "id:"
private const val SEARCH_LIST_PREFIX = "my:"
}
private val interceptor by lazy { MyAnimeListInterceptor(this) }
private val json: Json by injectLazy()
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
private val api by lazy { MyAnimeListApi(client, interceptor) }
override val name: String
get() = "MyAnimeList"
@StringRes
override fun nameRes() = R.string.tracker_myanimelist
override val supportsReadingDates: Boolean = true
@ -40,7 +41,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
override fun getLogoColor() = Color.rgb(46, 81, 162)
override fun getStatusList(): List<Int> {
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ)
return listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
}
override fun getStatus(status: Int): String = with(context) {
@ -50,6 +51,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
REREADING -> getString(R.string.repeating)
else -> ""
}
}
@ -64,77 +66,75 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString()
}
override fun add(track: Track): Observable<Track> {
return api.addLibManga(track)
override suspend fun add(track: Track): Track {
track.status = READING
track.score = 0F
return api.updateItem(track)
}
override fun update(track: Track): Observable<Track> {
return api.updateLibManga(track)
override suspend fun update(track: Track): Track {
return api.updateItem(track)
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track)
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findListItem(track)
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.media_id = remoteTrack.media_id
update(track)
} else {
add(track)
}
}
override suspend fun search(query: String): List<TrackSearch> {
if (query.startsWith(SEARCH_ID_PREFIX)) {
query.substringAfter(SEARCH_ID_PREFIX).toIntOrNull()?.let { id ->
return listOf(api.getMangaDetails(id))
}
}
}
if (query.startsWith(SEARCH_LIST_PREFIX)) {
query.substringAfter(SEARCH_LIST_PREFIX).let { title ->
return api.findListItems(title)
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.getLibManga(track)
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
track
}
override suspend fun refresh(track: Track): Track {
return api.findListItem(track) ?: add(track)
}
fun login(csrfToken: String): Completable = login("myanimelist", csrfToken)
override suspend fun login(username: String, password: String) = login(password)
override fun login(username: String, password: String): Completable {
return Observable.fromCallable { saveCSRF(password) }
.doOnNext { saveCredentials(username, password) }
.doOnError { logout() }
.toCompletable()
}
fun ensureLoggedIn() {
if (isAuthorized) return
if (!isLogged) throw Exception("MAL login credentials not found")
suspend fun login(authCode: String) {
try {
val oauth = api.getAccessToken(authCode)
interceptor.setAuth(oauth)
val username = api.getCurrentUser()
saveCredentials(username, oauth.access_token)
} catch (e: Throwable) {
logout()
}
}
override fun logout() {
super.logout()
preferences.trackToken(this).delete()
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
interceptor.setAuth(null)
}
private val isAuthorized: Boolean
get() = super.isLogged &&
getCSRF().isNotEmpty() &&
checkCookies()
fun saveOAuth(oAuth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oAuth))
}
fun getCSRF(): String = preferences.trackToken(this).get()
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
private fun checkCookies(): Boolean {
val url = BASE_URL.toHttpUrlOrNull()!!
val ckCount = networkService.cookieManager.get(url).count {
it.name == USER_SESSION_COOKIE || it.name == LOGGED_IN_COOKIE
fun loadOAuth(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
} catch (e: Exception) {
null
}
return ckCount == 2
}
}

View File

@ -1,472 +1,271 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.lang.toCalendar
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.PkceUtil
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import rx.Observable
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.zip.GZIPInputStream
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun search(query: String): Observable<List<TrackSearch>> {
return if (query.startsWith(PREFIX_MY)) {
val realQuery = query.removePrefix(PREFIX_MY)
getList()
.flatMap { Observable.from(it) }
.filter { it.title.contains(realQuery, true) }
.toList()
} else {
client.newCall(GET(searchUrl(query)))
.asObservable()
.flatMap { response ->
Observable.from(
Jsoup.parse(response.consumeBody())
.select("div.js-categories-seasonal.js-block-list.list")
.select("table").select("tbody")
.select("tr").drop(1)
)
suspend fun getAccessToken(authCode: String): OAuth {
return withIOContext {
val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId)
.add("code", authCode)
.add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code")
.build()
client.newCall(POST("$baseOAuthUrl/token", body = formBody))
.await()
.parseAs()
}
}
suspend fun getCurrentUser(): String {
return withIOContext {
val request = Request.Builder()
.url("$baseApiUrl/users/@me")
.get()
.build()
authClient.newCall(request)
.await()
.parseAs<JsonObject>()
.let { it["name"]!!.jsonPrimitive.content }
}
}
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon()
.appendQueryParameter("q", query)
.appendQueryParameter("nsfw", "true")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonObject>()
.let {
it["data"]!!.jsonArray
.map { data -> data.jsonObject["node"]!!.jsonObject }
.map { node ->
val id = node["id"]!!.jsonPrimitive.int
async { getMangaDetails(id) }
}
.awaitAll()
.filter { trackSearch -> trackSearch.publishing_type != "novel" }
}
.filter { row ->
row.select(TD)[2].text() != "Novel"
}
.map { row ->
}
}
suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon()
.appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonObject>()
.let {
val obj = it.jsonObject
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = row.searchTitle()
media_id = row.searchMediaId()
total_chapters = row.searchTotalChapters()
summary = row.searchSummary()
cover_url = row.searchCoverUrl()
tracking_url = mangaUrl(media_id)
publishing_status = row.searchPublishingStatus()
publishing_type = row.searchPublishingType()
start_date = row.searchStartDate()
}
}
.toList()
}
}
fun addLibManga(track: Track): Observable<Track> {
return Observable.defer {
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
.asObservableSuccess()
.map { track }
}
}
fun updateLibManga(track: Track): Observable<Track> {
return Observable.defer {
// Get track data
val response = authClient.newCall(GET(url = editPageUrl(track.media_id))).execute()
val editData = response.use {
val page = Jsoup.parse(it.consumeBody())
// Extract track data from MAL page
extractDataFromEditPage(page).apply {
// Apply changes to the just fetched data
copyPersonalFrom(track)
}
}
// Update remote
authClient.newCall(POST(url = editPageUrl(track.media_id), body = mangaEditPostBody(editData)))
.asObservableSuccess()
.map {
track
}
}
}
fun findLibManga(track: Track): Observable<Track?> {
return authClient.newCall(GET(url = editPageUrl(track.media_id)))
.asObservable()
.map { response ->
var libTrack: Track? = null
response.use {
if (it.priorResponse?.isRedirect != true) {
val trackForm = Jsoup.parse(it.consumeBody())
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
total_chapters = trackForm.select("#totalChap").text().toInt()
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull()
?: 0f
started_reading_date = trackForm.searchDatePicker("#add_manga_start_date")
finished_reading_date = trackForm.searchDatePicker("#add_manga_finish_date")
media_id = obj["id"]!!.jsonPrimitive.int
title = obj["title"]!!.jsonPrimitive.content
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: ""
tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ")
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
start_date = try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(obj["start_date"]!!)
} catch (e: Exception) {
""
}
}
}
libTrack
}
}
fun getLibManga(track: Track): Observable<Track> {
return findLibManga(track)
.map { it ?: throw Exception("Could not find manga") }
}
private fun getList(): Observable<List<TrackSearch>> {
return getListUrl()
.flatMap { url ->
getListXml(url)
}
.flatMap { doc ->
Observable.from(doc.select("manga"))
}
.map {
TrackSearch.create(TrackManager.MYANIMELIST).apply {
title = it.selectText("manga_title")!!
media_id = it.selectInt("manga_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = getStatus(it.selectText("my_status")!!)
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("manga_chapters")
tracking_url = mangaUrl(media_id)
started_reading_date = it.searchDateXml("my_start_date")
finished_reading_date = it.searchDateXml("my_finish_date")
}
}
.toList()
}
private fun getListUrl(): Observable<String> {
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
.asObservable()
.map { response ->
baseUrl + Jsoup.parse(response.consumeBody())
.select("div.goodresult")
.select("a")
.attr("href")
}
}
private fun getListXml(url: String): Observable<Document> {
return authClient.newCall(GET(url))
.asObservable()
.map { response ->
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
}
}
private fun Response.consumeBody(): String? {
use {
if (it.code != 200) throw Exception("HTTP error ${it.code}")
return it.body?.string()
}
}
private fun Response.consumeXmlBody(): String? {
use { res ->
if (res.code != 200) throw Exception("Export list error")
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
val sb = StringBuilder()
reader.forEachLine { line ->
sb.append(line)
suspend fun updateItem(track: Track): Track {
return withIOContext {
val formBodyBuilder = FormBody.Builder()
.add("status", track.toMyAnimeListStatus() ?: "reading")
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
.add("score", track.score.toString())
.add("num_chapters_read", track.last_chapter_read.toString())
convertToIsoDate(track.started_reading_date)?.let {
formBodyBuilder.add("start_date", it)
}
convertToIsoDate(track.finished_reading_date)?.let {
formBodyBuilder.add("finish_date", it)
}
val request = Request.Builder()
.url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build())
.build()
authClient.newCall(request)
.await()
.parseAs<JsonObject>()
.let { parseMangaItem(it, track) }
}
}
suspend fun findListItem(track: Track): Track? {
return withIOContext {
val uri = "$baseApiUrl/manga".toUri().buildUpon()
.appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build()
authClient.newCall(GET(uri.toString()))
.await()
.parseAs<JsonObject>()
.let { obj ->
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
obj.jsonObject["my_list_status"]?.jsonObject?.let {
parseMangaItem(it, track)
}
}
return sb.toString()
}
}
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
return withIOContext {
val json = getListPage(offset)
val obj = json.jsonObject
val matches = obj["data"]!!.jsonArray
.filter {
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
query,
ignoreCase = true
)
}
.map {
val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int
async { getMangaDetails(id) }
}
.awaitAll()
// Check next page if there's more
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
matches + findListItems(query, offset + listPaginationAmount)
} else {
matches
}
}
}
private fun extractDataFromEditPage(page: Document): MyAnimeListEditData {
val tables = page.select("form#main-form table")
private suspend fun getListPage(offset: Int): JsonObject {
return withIOContext {
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon()
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
.appendQueryParameter("limit", listPaginationAmount.toString())
if (offset > 0) {
urlBuilder.appendQueryParameter("offset", offset.toString())
}
return MyAnimeListEditData(
entry_id = tables[0].select("input[name=entry_id]").`val`(), // Always 0
manga_id = tables[0].select("#manga_id").`val`(),
status = tables[0].select("#add_manga_status > option[selected]").`val`(),
num_read_volumes = tables[0].select("#add_manga_num_read_volumes").`val`(),
last_completed_vol = tables[0].select("input[name=last_completed_vol]").`val`(), // Always empty
num_read_chapters = tables[0].select("#add_manga_num_read_chapters").`val`(),
score = tables[0].select("#add_manga_score > option[selected]").`val`(),
start_date_month = tables[0].select("#add_manga_start_date_month > option[selected]").`val`(),
start_date_day = tables[0].select("#add_manga_start_date_day > option[selected]").`val`(),
start_date_year = tables[0].select("#add_manga_start_date_year > option[selected]").`val`(),
finish_date_month = tables[0].select("#add_manga_finish_date_month > option[selected]").`val`(),
finish_date_day = tables[0].select("#add_manga_finish_date_day > option[selected]").`val`(),
finish_date_year = tables[0].select("#add_manga_finish_date_year > option[selected]").`val`(),
tags = tables[1].select("#add_manga_tags").`val`(),
priority = tables[1].select("#add_manga_priority > option[selected]").`val`(),
storage_type = tables[1].select("#add_manga_storage_type > option[selected]").`val`(),
num_retail_volumes = tables[1].select("#add_manga_num_retail_volumes").`val`(),
num_read_times = tables[1].select("#add_manga_num_read_times").`val`(),
reread_value = tables[1].select("#add_manga_reread_value > option[selected]").`val`(),
comments = tables[1].select("#add_manga_comments").`val`(),
is_asked_to_discuss = tables[1].select("#add_manga_is_asked_to_discuss > option[selected]").`val`(),
sns_post_type = tables[1].select("#add_manga_sns_post_type > option[selected]").`val`()
)
val request = Request.Builder()
.url(urlBuilder.build().toString())
.get()
.build()
authClient.newCall(request)
.await()
.parseAs()
}
}
private fun parseMangaItem(response: JsonObject, track: Track): Track {
val obj = response.jsonObject
return track.apply {
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.int
score = obj["score"]!!.jsonPrimitive.int.toFloat()
obj["start_date"]?.let {
started_reading_date = parseDate(it.jsonPrimitive.content)
}
obj["finish_date"]?.let {
finished_reading_date = parseDate(it.jsonPrimitive.content)
}
}
}
private fun parseDate(isoDate: String): Long {
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
}
private fun convertToIsoDate(epochTime: Long): String? {
if (epochTime == 0L) {
return ""
}
return try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(epochTime)
} catch (e: Exception) {
null
}
}
companion object {
const val CSRF = "csrf_token"
// Registered under arkon's MAL account
private const val clientId = "8fd3313bc138e8b890551aa1de1a2589"
private const val baseUrl = "https://myanimelist.net"
private const val baseMangaUrl = "$baseUrl/manga/"
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
private const val PREFIX_MY = "my:"
private const val TD = "td"
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2"
private const val baseApiUrl = "https://api.myanimelist.net/v2"
fun loginUrl() = baseUrl.toUri().buildUpon()
.appendPath("login.php")
.toString()
private const val listPaginationAmount = 250
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private var codeVerifier: String = ""
private fun searchUrl(query: String): String {
val col = "c[]"
return baseUrl.toUri().buildUpon()
.appendPath("manga.php")
.appendQueryParameter("q", query)
.appendQueryParameter(col, "a")
.appendQueryParameter(col, "b")
.appendQueryParameter(col, "c")
.appendQueryParameter(col, "d")
.appendQueryParameter(col, "e")
.appendQueryParameter(col, "g")
.toString()
}
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("code_challenge", getPkceChallengeCode())
.appendQueryParameter("response_type", "code")
.build()
private fun exportListUrl() = baseUrl.toUri().buildUpon()
.appendPath("panel.php")
.appendQueryParameter("go", "export")
.toString()
fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon()
.appendPath(id.toString())
.appendPath("my_list_status")
.build()
private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
.appendPath(mediaId.toString())
.appendPath("edit")
.toString()
private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
.appendPath("add.json")
.toString()
private fun exportPostBody(): RequestBody {
return FormBody.Builder()
.add("type", "2")
.add("subexport", "Export My List")
fun refreshTokenRequest(refreshToken: String): Request {
val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId)
.add("refresh_token", refreshToken)
.add("grant_type", "refresh_token")
.build()
return POST("$baseOAuthUrl/token", body = formBody)
}
private fun mangaPostPayload(track: Track): RequestBody {
val body = JSONObject()
.put("manga_id", track.media_id)
.put("status", track.status)
.put("score", track.score)
.put("num_read_chapters", track.last_chapter_read)
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
}
private fun mangaEditPostBody(track: MyAnimeListEditData): RequestBody {
return FormBody.Builder()
.add("entry_id", track.entry_id)
.add("manga_id", track.manga_id)
.add("add_manga[status]", track.status)
.add("add_manga[num_read_volumes]", track.num_read_volumes)
.add("last_completed_vol", track.last_completed_vol)
.add("add_manga[num_read_chapters]", track.num_read_chapters)
.add("add_manga[score]", track.score)
.add("add_manga[start_date][month]", track.start_date_month)
.add("add_manga[start_date][day]", track.start_date_day)
.add("add_manga[start_date][year]", track.start_date_year)
.add("add_manga[finish_date][month]", track.finish_date_month)
.add("add_manga[finish_date][day]", track.finish_date_day)
.add("add_manga[finish_date][year]", track.finish_date_year)
.add("add_manga[tags]", track.tags)
.add("add_manga[priority]", track.priority)
.add("add_manga[storage_type]", track.storage_type)
.add("add_manga[num_retail_volumes]", track.num_retail_volumes)
.add("add_manga[num_read_times]", track.num_read_times)
.add("add_manga[reread_value]", track.reread_value)
.add("add_manga[comments]", track.comments)
.add("add_manga[is_asked_to_discuss]", track.is_asked_to_discuss)
.add("add_manga[sns_post_type]", track.sns_post_type)
.add("submitIt", track.submitIt)
.build()
}
private fun Element.searchDateXml(field: String): Long {
val text = selectText(field, "0000-00-00")!!
// MAL sets the data to 0000-00-00 when date is invalid or missing
if (text == "0000-00-00") {
return 0L
}
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(text)?.time ?: 0L
}
private fun Element.searchDatePicker(id: String): Long {
val month = select(id + "_month > option[selected]").`val`().toIntOrNull()
val day = select(id + "_day > option[selected]").`val`().toIntOrNull()
val year = select(id + "_year > option[selected]").`val`().toIntOrNull()
if (year == null || month == null || day == null) {
return 0L
}
return GregorianCalendar(year, month - 1, day).timeInMillis
}
private fun Element.searchTitle() = select("strong").text()!!
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
private fun Element.searchCoverUrl() = select("img")
.attr("data-src")
.split("\\?")[0]
.replace("/r/50x70/", "/")
private fun Element.searchMediaId() = select("div.picSurround")
.select("a").attr("id")
.replace("sarea", "")
.toInt()
private fun Element.searchSummary() = select("div.pt4")
.first()
.ownText()!!
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
private fun Element.searchPublishingType() = select(TD)[2].text()!!
private fun Element.searchStartDate() = select(TD)[6].text()!!
private fun getStatus(status: String) = when (status) {
"Reading" -> 1
"Completed" -> 2
"On-Hold" -> 3
"Dropped" -> 4
"Plan to Read" -> 6
else -> 1
}
}
private class MyAnimeListEditData(
// entry_id
var entry_id: String,
// manga_id
var manga_id: String,
// add_manga[status]
var status: String,
// add_manga[num_read_volumes]
var num_read_volumes: String,
// last_completed_vol
var last_completed_vol: String,
// add_manga[num_read_chapters]
var num_read_chapters: String,
// add_manga[score]
var score: String,
// add_manga[start_date][month]
var start_date_month: String, // [1-12]
// add_manga[start_date][day]
var start_date_day: String,
// add_manga[start_date][year]
var start_date_year: String,
// add_manga[finish_date][month]
var finish_date_month: String, // [1-12]
// add_manga[finish_date][day]
var finish_date_day: String,
// add_manga[finish_date][year]
var finish_date_year: String,
// add_manga[tags]
var tags: String,
// add_manga[priority]
var priority: String,
// add_manga[storage_type]
var storage_type: String,
// add_manga[num_retail_volumes]
var num_retail_volumes: String,
// add_manga[num_read_times]
var num_read_times: String,
// add_manga[reread_value]
var reread_value: String,
// add_manga[comments]
var comments: String,
// add_manga[is_asked_to_discuss]
var is_asked_to_discuss: String,
// add_manga[sns_post_type]
var sns_post_type: String,
// submitIt
val submitIt: String = "0"
) {
fun copyPersonalFrom(track: Track) {
num_read_chapters = track.last_chapter_read.toString()
val numScore = track.score.toInt()
if (numScore == 0) {
score = ""
} else if (numScore in 1..10) {
score = numScore.toString()
}
status = track.status.toString()
if (track.started_reading_date == 0L) {
start_date_month = ""
start_date_day = ""
start_date_year = ""
}
if (track.finished_reading_date == 0L) {
finish_date_month = ""
finish_date_day = ""
finish_date_year = ""
}
track.started_reading_date.toCalendar()?.let { cal ->
start_date_month = (cal[Calendar.MONTH] + 1).toString()
start_date_day = cal[Calendar.DAY_OF_MONTH].toString()
start_date_year = cal[Calendar.YEAR].toString()
}
track.finished_reading_date.toCalendar()?.let { cal ->
finish_date_month = (cal[Calendar.MONTH] + 1).toString()
finish_date_day = cal[Calendar.DAY_OF_MONTH].toString()
finish_date_year = cal[Calendar.YEAR].toString()
}
private fun getPkceChallengeCode(): String {
codeVerifier = PkceUtil.generateCodeVerifier()
return codeVerifier
}
}
}

View File

@ -1,52 +1,58 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.Buffer
import org.json.JSONObject
import uy.kohesive.injekt.injectLazy
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
private val json: Json by injectLazy()
private var oauth: OAuth? = null
set(value) {
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
}
override fun intercept(chain: Interceptor.Chain): Response {
myanimelist.ensureLoggedIn()
val originalRequest = chain.request()
val request = chain.request()
return chain.proceed(updateRequest(request))
}
private fun updateRequest(request: Request): Request {
return request.body?.let {
val contentType = it.contentType().toString()
val updatedBody = when {
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
contentType.contains("json") -> updateJsonBody(it)
else -> it
}
request.newBuilder().post(updatedBody).build()
} ?: request
}
private fun bodyToString(requestBody: RequestBody): String {
Buffer().use {
requestBody.writeTo(it)
return it.readUtf8()
if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with MyAnimeList")
}
if (oauth == null) {
oauth = myanimelist.loadOAuth()
}
// Refresh access token if null or expired.
if (oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
if (it.isSuccessful) {
setAuth(json.decodeFromString(it.body!!.string()))
}
}
}
// Throw on null auth.
if (oauth == null) {
throw Exception("No authentication token")
}
// Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build()
return chain.proceed(authRequest)
}
private fun updateFormBody(requestBody: RequestBody): RequestBody {
val formString = bodyToString(requestBody)
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(requestBody.contentType())
}
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
val jsonString = bodyToString(requestBody)
val newBody = JSONObject(jsonString)
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
return newBody.toString().toRequestBody(requestBody.contentType())
/**
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
* and the oauth object.
*/
fun setAuth(oauth: OAuth?) {
token = oauth?.access_token
this.oauth = oauth
myanimelist.saveOAuth(oauth)
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.data.database.models.Track
fun Track.toMyAnimeListStatus() = when (status) {
MyAnimeList.READING -> "reading"
MyAnimeList.COMPLETED -> "completed"
MyAnimeList.ON_HOLD -> "on_hold"
MyAnimeList.DROPPED -> "dropped"
MyAnimeList.PLAN_TO_READ -> "plan_to_read"
MyAnimeList.REREADING -> "reading"
else -> null
}
fun getStatus(status: String) = when (status) {
"reading" -> MyAnimeList.READING
"completed" -> MyAnimeList.COMPLETED
"on_hold" -> MyAnimeList.ON_HOLD
"dropped" -> MyAnimeList.DROPPED
"plan_to_read" -> MyAnimeList.PLAN_TO_READ
else -> MyAnimeList.READING
}

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.data.track.myanimelist
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(
val refresh_token: String,
val access_token: String,
val token_type: String,
val expires_in: Long
) {
fun isExpired() = System.currentTimeMillis() > expires_in
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.shikimori
import android.content.Context
import android.graphics.Color
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
@ -9,8 +10,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
@ -22,19 +21,17 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
const val DROPPED = 4
const val PLANNING = 5
const val REPEATING = 6
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "Shikimori"
private val json: Json by injectLazy()
private val interceptor by lazy { ShikimoriInterceptor(this) }
private val api by lazy { ShikimoriApi(client, interceptor) }
@StringRes
override fun nameRes() = R.string.tracker_shikimori
override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString)
}
@ -43,43 +40,38 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
return track.score.toInt().toString()
}
override fun add(track: Track): Observable<Track> {
override suspend fun add(track: Track): Track {
return api.addLibManga(track, getUsername())
}
override fun update(track: Track): Observable<Track> {
override suspend fun update(track: Track): Track {
return api.updateLibManga(track, getUsername())
}
override fun bind(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername())
.flatMap { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.score = DEFAULT_SCORE.toFloat()
track.status = DEFAULT_STATUS
add(track)
}
}
override suspend fun bind(track: Track): Track {
val remoteTrack = api.findLibManga(track, getUsername())
return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
update(track)
} else {
// Set default fields if it's not found in the list
track.status = READING
track.score = 0F
add(track)
}
}
override fun search(query: String): Observable<List<TrackSearch>> {
override suspend fun search(query: String): List<TrackSearch> {
return api.search(query)
}
override fun refresh(track: Track): Observable<Track> {
return api.findLibManga(track, getUsername())
.map { remoteTrack ->
if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
}
track
}
override suspend fun refresh(track: Track): Track {
api.findLibManga(track, getUsername())?.let { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = remoteTrack.total_chapters
}
return track
}
override fun getLogo() = R.drawable.ic_tracker_shikimori
@ -104,18 +96,17 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun getCompletionStatus(): Int = COMPLETED
override fun login(username: String, password: String) = login(password)
override suspend fun login(username: String, password: String) = login(password)
fun login(code: String): Completable {
return api.accessToken(code).map { oauth: OAuth? ->
suspend fun login(code: String) {
try {
val oauth = api.accessToken(code)
interceptor.newAuth(oauth)
if (oauth != null) {
val user = api.getCurrentUser()
saveCredentials(user.toString(), oauth.access_token)
}
}.doOnError {
val user = api.getCurrentUser()
saveCredentials(user.toString(), oauth.access_token)
} catch (e: Throwable) {
logout()
}.toCompletable()
}
}
fun saveToken(oauth: OAuth?) {

View File

@ -6,9 +6,11 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
@ -19,65 +21,53 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
private val json: Json by injectLazy()
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track, user_id: String): Observable<Track> {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("target_id", track.media_id)
put("target_type", "Manga")
put("chapters", track.last_chapter_read)
put("score", track.score.toInt())
put("status", track.toShikimoriStatus())
suspend fun addLibManga(track: Track, user_id: String): Track {
return withIOContext {
val payload = buildJsonObject {
putJsonObject("user_rate") {
put("user_id", user_id)
put("target_id", track.media_id)
put("target_type", "Manga")
put("chapters", track.last_chapter_read)
put("score", track.score.toInt())
put("status", track.toShikimoriStatus())
}
}
authClient.newCall(
POST(
"$apiUrl/v2/user_rates",
body = payload.toString().toRequestBody(jsonMime)
)
).await()
track
}
val body = payload.toString().toRequestBody(jsonime)
val request = Request.Builder()
.url("$apiUrl/v2/user_rates")
.post(body)
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map {
track
}
}
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
fun search(search: String): Observable<List<TrackSearch>> {
val url = "$apiUrl/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
return authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val url = "$apiUrl/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search)
.appendQueryParameter("limit", "20")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonArray>()
.let { response ->
response.map {
jsonToSearch(it.jsonObject)
}
}
val response = json.decodeFromString<JsonArray>(responseBody)
response.map { jsonToSearch(it.jsonObject) }
}
}
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
@ -106,61 +96,51 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
}
}
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
val request = Request.Builder()
.url(url.toString())
.get()
.build()
suspend fun findLibManga(track: Track, user_id: String): Track? {
return withIOContext {
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build()
val mangas = authClient.newCall(GET(urlMangas.toString()))
.await()
.parseAs<JsonObject>()
val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString())
.build()
val requestMangas = Request.Builder()
.url(urlMangas.toString())
.get()
.build()
return authClient.newCall(requestMangas)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
json.decodeFromString<JsonObject>(responseBody)
}.flatMap { mangas ->
authClient.newCall(request)
.asObservableSuccess()
.map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = json.decodeFromString<JsonArray>(responseBody)
if (response.size > 1) {
throw Exception("Too much mangas in response")
}
val entry = response.map {
jsonToTrack(it.jsonObject, mangas)
}
entry.firstOrNull()
val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga")
.build()
authClient.newCall(GET(url.toString()))
.await()
.parseAs<JsonArray>()
.let { response ->
if (response.size > 1) {
throw Exception("Too much mangas in response")
}
}
val entry = response.map {
jsonToTrack(it.jsonObject, mangas)
}
entry.firstOrNull()
}
}
}
fun getCurrentUser(): Int {
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()!!
return json.decodeFromString<JsonObject>(user)["id"]!!.jsonPrimitive.int
return runBlocking {
authClient.newCall(GET("$apiUrl/users/whoami"))
.await()
.parseAs<JsonObject>()
.let {
it["id"]!!.jsonPrimitive.int
}
}
}
fun accessToken(code: String): Observable<OAuth> {
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
val responseBody = netResponse.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
json.decodeFromString<OAuth>(responseBody)
suspend fun accessToken(code: String): OAuth {
return withIOContext {
client.newCall(accessTokenRequest(code))
.await()
.parseAs()
}
}

View File

@ -9,7 +9,7 @@ fun Track.toShikimoriStatus() = when (status) {
Shikimori.DROPPED -> "dropped"
Shikimori.PLANNING -> "planned"
Shikimori.REPEATING -> "rewatching"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
@ -19,6 +19,5 @@ fun toTrackStatus(status: String) = when (status) {
"dropped" -> Shikimori.DROPPED
"planned" -> Shikimori.PLANNING
"rewatching" -> Shikimori.REPEATING
else -> throw Exception("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.data.updater.github
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.http.Path
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* Used to connect with the GitHub API to get the latest release version from a repo.
*/
interface GithubService {
companion object {
fun create(): GithubService {
val restAdapter = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType()))
.client(Injekt.get<NetworkHelper>().client)
.build()
return restAdapter.create(GithubService::class.java)
}
}
@GET("/repos/{repo}/releases/latest")
suspend fun getLatestVersion(@Path("repo", encoded = true) repo: String): GithubRelease
}

View File

@ -2,27 +2,39 @@ package eu.kanade.tachiyomi.data.updater.github
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
class GithubUpdateChecker {
private val service: GithubService = GithubService.create()
private val networkService: NetworkHelper by injectLazy()
private val repo: String by lazy {
if (BuildConfig.DEBUG) {
"tachiyomiorg/android-app-preview"
"tachiyomiorg/tachiyomi-preview"
} else {
"inorichi/tachiyomi"
"tachiyomiorg/tachiyomi"
}
}
suspend fun checkForUpdate(): UpdateResult {
val release = service.getLatestVersion(repo)
// Check if latest version is different from current version
return if (isNewVersion(release.version)) {
GithubUpdateResult.NewUpdate(release)
} else {
GithubUpdateResult.NoNewUpdate()
return withIOContext {
networkService.client
.newCall(GET("https://api.github.com/repos/$repo/releases/latest"))
.await()
.parseAs<GithubRelease>()
.let {
// Check if latest version is different from current version
if (isNewVersion(it.version)) {
GithubUpdateResult.NewUpdate(it)
} else {
GithubUpdateResult.NoNewUpdate()
}
}
}
}
@ -31,11 +43,11 @@ class GithubUpdateChecker {
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.DEBUG) {
// Preview builds: based on releases in "tachiyomiorg/android-app-preview" repo
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
// tagged as something like "r1234"
newVersion.toInt() > BuildConfig.COMMIT_COUNT.toInt()
} else {
// Release builds: based on releases in "inorichi/tachiyomi" repo
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2"
newVersion != BuildConfig.VERSION_NAME
}

View File

@ -124,8 +124,7 @@ class ExtensionManager(
.map { it.extension }
installedExtensions
.flatMap { it.sources }
// overwrite is needed until the bundled sources are removed
.forEach { sourceManager.registerSource(it, true) }
.forEach { sourceManager.registerSource(it) }
untrustedExtensions = extensions
.filterIsInstance<LoadResult.Untrusted>()

View File

@ -5,8 +5,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
@ -16,14 +19,16 @@ import java.util.Date
internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
return withContext(Dispatchers.IO) {
val response = service.getRepo()
parseResponse(response)
return withIOContext {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
.parseAs<JsonArray>()
.let { parseResponse(it) }
}
}
@ -65,18 +70,18 @@ internal class ExtensionGithubApi {
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
}
companion object {
const val BASE_URL = "https://raw.githubusercontent.com/"
const val REPO_URL_PREFIX = "${BASE_URL}inorichi/tachiyomi-extensions/repo/"
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.extension.api
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
fun create(): ExtensionGithubService {
val network: NetworkHelper by injectLazy()
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.client(network.client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}index.min.json")
suspend fun getRepo(): JsonArray
}

View File

@ -18,6 +18,7 @@ sealed class Extension {
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val pkgFactory: String?,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,

View File

@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
@ -26,12 +25,13 @@ import uy.kohesive.injekt.injectLazy
internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy()
private val allowNsfwSource by lazy {
preferences.allowNsfwSource().get()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
@ -133,7 +133,7 @@ internal object ExtensionLoader {
}
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
if (!loadNsfwSource && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed")
}
@ -185,7 +185,8 @@ internal object ExtensionLoader {
versionCode,
lang,
isNsfw,
sources,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature
)
return LoadResult.Success(extension)
@ -218,7 +219,7 @@ internal object ExtensionLoader {
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
if (loadNsfwSource) {
return false
}

View File

@ -99,7 +99,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent")
?: HttpSource.DEFAULT_USERAGENT
?: HttpSource.DEFAULT_USER_AGENT
webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) {

View File

@ -1,19 +1,27 @@
package eu.kanade.tachiyomi.network
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
val jsonMime = "application/json; charset=utf-8".toMediaType()
fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
@ -52,12 +60,12 @@ fun Call.asObservable(): Observable<Response> {
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
suspend fun Call.await(assertSuccess: Boolean = false): Response {
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
if (assertSuccess && !response.isSuccessful) {
if (!response.isSuccessful) {
continuation.resumeWithException(Exception("HTTP error ${response.code}"))
return
}
@ -105,3 +113,12 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
return progressClient.newCall(request)
}
inline fun <reified T> Response.parseAs(): T {
// Avoiding Injekt.get<Json>() due to compiler issues
val json = Injekt.getInstance<Json>(fullType<Json>().type)
this.use {
val responseBody = it.body?.string().orEmpty()
return json.decodeFromString(responseBody)
}
}

View File

@ -12,7 +12,7 @@ class UserAgentInterceptor : Interceptor {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USERAGENT)
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest)
} else {

View File

@ -62,14 +62,18 @@ interface Source : tachiyomi.source.Source {
/**
* [1.x API] Get the updated details for a manga.
*/
@Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return fetchMangaDetails(manga.toSManga()).awaitSingle()
.toMangaInfo()
val sManga = manga.toSManga()
val networkManga = fetchMangaDetails(sManga).awaitSingle()
sManga.copyFrom(networkManga)
return sManga.toMangaInfo()
}
/**
* [1.x API] Get all the available chapters for a manga.
*/
@Suppress("DEPRECATION")
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return fetchChapterList(manga.toSManga()).awaitSingle()
.map { it.toChapterInfo() }
@ -78,6 +82,7 @@ interface Source : tachiyomi.source.Source {
/**
* [1.x API] Get the list of pages a chapter has.
*/
@Suppress("DEPRECATION")
override suspend fun getPageList(chapter: ChapterInfo): List<tachiyomi.source.model.Page> {
return fetchPageList(chapter.toSChapter()).awaitSingle()
.map { it.toPageUrl() }

View File

@ -32,11 +32,11 @@ open class SourceManager(private val context: Context) {
fun getCatalogueSources() = sourcesMap.values.filterIsInstance<CatalogueSource>()
internal fun registerSource(source: Source, overwrite: Boolean = false) {
if (overwrite || !sourcesMap.containsKey(source.id)) {
internal fun registerSource(source: Source) {
if (!sourcesMap.containsKey(source.id)) {
sourcesMap[source.id] = source
}
if (overwrite || !stubSourcesMap.containsKey(source.id)) {
if (!stubSourcesMap.containsKey(source.id)) {
stubSourcesMap[source.id] = StubSource(source.id)
}
}
@ -49,7 +49,7 @@ open class SourceManager(private val context: Context) {
LocalSource(context)
)
private inner class StubSource(override val id: Long) : Source {
inner class StubSource(override val id: Long) : Source {
override val name: String
get() = id.toString()

View File

@ -74,7 +74,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USERAGENT)
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
@ -371,6 +371,6 @@ abstract class HttpSource : CatalogueSource {
override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
}
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
@ -13,7 +12,6 @@ abstract class BaseRxActivity<VB : ViewBinding, P : BasePresenter<*>> : NucleusA
@Suppress("LeakingThis")
private val secureActivityDelegate = SecureActivityDelegate(this)
val scope = lifecycleScope
lateinit var binding: VB
init {

View File

@ -4,25 +4,15 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy()
val scope = lifecycleScope
lateinit var binding: VB
@Suppress("LeakingThis")
private val secureActivityDelegate = SecureActivityDelegate(this)
private val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) ||
@ -62,11 +52,6 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
}
}
init {
@Suppress("LeakingThis")
LocaleHelper.updateConfiguration(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(
when {
@ -76,13 +61,5 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
)
super.onCreate(savedInstanceState)
secureActivityDelegate.onCreate()
}
override fun onResume() {
super.onResume()
secureActivityDelegate.onResume()
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.os.Bundle
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
abstract class BaseViewBindingActivity<VB : ViewBinding> : BaseThemedActivity() {
lateinit var binding: VB
@Suppress("LeakingThis")
private val secureActivityDelegate = SecureActivityDelegate(this)
init {
@Suppress("LeakingThis")
LocaleHelper.updateConfiguration(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
secureActivityDelegate.onCreate()
}
override fun onResume() {
super.onResume()
secureActivityDelegate.onResume()
}
}

View File

@ -11,16 +11,18 @@ import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.clearFindViewByIdCache
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle),
LayoutContainer {
RestoreViewOnCreateController(bundle) {
lateinit var binding: VB
lateinit var viewScope: CoroutineScope
init {
addLifecycleListener(
object : LifecycleListener() {
@ -29,6 +31,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
}
override fun preCreateView(controller: Controller) {
viewScope = MainScope()
Timber.d("Create view for ${controller.instance()}")
}
@ -41,24 +44,17 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
}
override fun preDestroyView(controller: Controller, view: View) {
viewScope.cancel()
Timber.d("Destroy view for ${controller.instance()}")
}
}
)
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
return inflateView(inflater, container)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View) {}

View File

@ -4,9 +4,6 @@ import android.os.Bundle
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorDelegate
import eu.kanade.tachiyomi.ui.base.presenter.NucleusConductorLifecycleListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter
@ -17,8 +14,6 @@ abstract class NucleusController<VB : ViewBinding, P : Presenter<*>>(val bundle:
private val delegate = NucleusConductorDelegate(this)
val scope = CoroutineScope(Job() + Dispatchers.Main)
val presenter: P
get() = delegate.presenter!!

View File

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView
}

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