Compare commits

...

117 Commits

Author SHA1 Message Date
2b38b4e022 Release v0.13.2 2022-04-10 12:17:45 -04:00
4ecde9fc39 Gate update/download warnings to non-stable flavors 2022-04-10 12:17:45 -04:00
445ee274c5 Update to AGP 7.1.3 2022-04-10 12:17:45 -04:00
f2bdc514e8 Weblate translations (#6829)
Co-authored-by: Ahmad Azwar Annas <ahmadazw2@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hitalo | イタチ <Hitalomarquete331@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: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Jozef Hollý <j2.00ghz@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicol Bolas <creepyweirdo1031@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pierre Kim <admin@manateeshome.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Respek <pedjal3345@gmail.com>
Co-authored-by: Rick <rickeits153@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Leonardo <lafruta94@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: THElegend5 <jindalpratik98@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: אילון קטן <eilonkatan@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/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
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/he/
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/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/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sa/
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/th/
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/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ahmad Azwar Annas <ahmadazw2@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hitalo | イタチ <Hitalomarquete331@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nicol Bolas <creepyweirdo1031@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Pierre Kim <admin@manateeshome.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Respek <pedjal3345@gmail.com>
Co-authored-by: Rick <rickeits153@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Leonardo <lafruta94@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: THElegend5 <jindalpratik98@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: אילון קטן <eilonkatan@gmail.com>
2022-04-09 09:42:33 -04:00
5afff31f72 Formatting 2022-04-08 16:44:23 -04:00
2dfafa387b Remove reader tapping option in favor of disabled nav layouts 2022-04-08 16:44:13 -04:00
7318f4f5dd Remove some dead code 2022-04-08 16:32:34 -04:00
175b77fe6f Add option to disable navigation layout (#6876) 2022-04-08 16:32:25 -04:00
346652e508 Ensure media store scan is triggered after saving an image (fixes #6808) 2022-04-08 15:55:12 -04:00
f0eb42e72d Update linter 2022-04-08 15:30:39 -04:00
37100f0937 Move delete action to match placement in library_selection.xml (#6869)
Move delete icon to far right in chapter_selection.xml and updates_chapter_selection.xml, for consistency with library_selection.xml
2022-04-08 12:10:59 -04:00
ac980a4dbf MangaCoverFetcher: Handle moving cover cache after adding to library (#6885)
Move cover cache to separate cache dir after the parent manga is added to library
2022-04-08 12:10:06 -04:00
a8b53499af Remove kotlin.compiler.execution.strategy config 2022-04-07 22:34:25 -04:00
a8aeae329e Bump to Gradle 7.4.2 2022-04-07 22:31:20 -04:00
52911539b8 Bump dependencies 2022-04-07 22:19:31 -04:00
3026ff241b Write library cover to library cover cache (#6883) 2022-04-07 22:00:17 -04:00
2466a079d5 MangaCoverFetcher: Don't close network response (#6882) 2022-04-07 13:34:31 -04:00
ed9fdf49e2 Add missing percent placeholder in some singular strings. (#6855) 2022-04-02 19:47:47 -04:00
668d962233 Update WebView requester package name
https://github.com/tachiyomiorg/tachiyomi/issues/6781#issuecomment-1086665483
2022-04-02 12:04:20 -04:00
996f770935 Override X-Requested-With header value in WebView requests (closes #6781) 2022-04-02 10:49:42 -04:00
041a6dd919 Update Coil 2022-04-02 09:55:25 -04:00
dbad60d03b Base activities cleanup (#6848)
* secure delegate

* theming delegate
2022-04-02 09:54:21 -04:00
27a60423dc Remove source filter sheet solid background (#6850)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-04-02 09:50:26 -04:00
5a37d38a84 Stop global search items from clipping (#6851)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-04-02 09:50:07 -04:00
6f566e67d5 Removed scrollbar on long theme item titles (#6852)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-04-02 09:49:50 -04:00
dd490f2ac9 Fix DST issue (#6831) 2022-04-02 08:52:53 -04:00
5409af0a6c MangaCoverFetcher: Use source's header for network request (#6847) 2022-04-02 08:44:01 -04:00
0ed0d903cc Force default browser for tracker logins
To avoid potentially opening up third party apps, which aren't useful for handling OAuth login flows.
2022-03-26 16:35:14 -04:00
85be4c492d Fix clear database selection toggling (fixes #6807) 2022-03-26 16:12:15 -04:00
c06ad8b87e Stop using custom tabs (closes #6821) 2022-03-26 15:45:58 -04:00
b89acb5853 Stop removing local manga's title from chapter names (closes #6578)
Users should better curate their chapter folder/file names if need be. There's legit reasons for a chapter to start with or contain the same word(s) that the manga title consists of.
2022-03-26 15:34:53 -04:00
7890511a53 Update dependencies 2022-03-26 15:23:31 -04:00
3aa4e6eb93 Add "Move all chapters from series to top" option to download context menu (#6794)
* Added basic move to top series feature

* Remove intermediate List

* Change text string

* Remove spanish manual translation

* Changed algorithm to use "partition"
2022-03-26 14:49:37 -04:00
f8eb9f94f4 Fix filename not having chapter title and page when sharing (#6827) 2022-03-26 12:40:29 -04:00
c581b9eeb9 Weblate translations (#6770)
Co-authored-by: Ahmad Azwar Annas <ahmadazw2@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Amir <amir.batyrggaliev@gmail.com>
Co-authored-by: Andi Firanda <jargonnation@gmail.com>
Co-authored-by: Anupam Malhotra <anpm.malhotra@gmail.com>
Co-authored-by: Artur Iwański <iartur221@gmail.com>
Co-authored-by: Aurimas Jurevičius <aurimasjurevic@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
Co-authored-by: Davit Gogritchiani <davitgogritchiani@outlook.com>
Co-authored-by: Drown by wind <ziemelis.martynas01@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: HouseDrVenus <aurimasjurevic@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jaime Martín <jaimemr06@gmail.com>
Co-authored-by: Jendrej <ejjendrej@gmail.com>
Co-authored-by: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Justina P <justuke08@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Madddog1997 <madddog1997@gmail.com>
Co-authored-by: Manoj Phuyal <manoj.phuye23@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Muhammad Diponegoro <dipoengoro@outlook.com>
Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Co-authored-by: Noemkinator <noemka1234@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Samuel Leonardo <lafruta94@gmail.com>
Co-authored-by: Sayykii <martin40lmg@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Veysel <jdksoalalskd71@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: gimme some socks <bobteen1@gmail.com>
Co-authored-by: mahdi eslam panah <mahdii3375@gmail.com>
Co-authored-by: mateus zampol <mateuszampol2009@hotmail.it>
Co-authored-by: saturn <swagburritovg@gmail.com>
Co-authored-by: typek52 <typek52@gmail.com>
Co-authored-by: xmdb <klchiu721@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ka/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/km/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
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/ne/
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/sa/
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/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
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/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ahmad Azwar Annas <ahmadazw2@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Amir <amir.batyrggaliev@gmail.com>
Co-authored-by: Andi Firanda <jargonnation@gmail.com>
Co-authored-by: Anupam Malhotra <anpm.malhotra@gmail.com>
Co-authored-by: Artur Iwański <iartur221@gmail.com>
Co-authored-by: Aurimas Jurevičius <aurimasjurevic@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: DatTran MLL <tranthanhdat1142003@gmail.com>
Co-authored-by: Davit Gogritchiani <davitgogritchiani@outlook.com>
Co-authored-by: Drown by wind <ziemelis.martynas01@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jaime Martín <jaimemr06@gmail.com>
Co-authored-by: Jendrej <ejjendrej@gmail.com>
Co-authored-by: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Justina P <justuke08@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Madddog1997 <madddog1997@gmail.com>
Co-authored-by: Manoj Phuyal <manoj.phuye23@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Muhammad Diponegoro <dipoengoro@outlook.com>
Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Co-authored-by: Noemkinator <noemka1234@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Samuel Leonardo <lafruta94@gmail.com>
Co-authored-by: Sayykii <martin40lmg@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Veysel <jdksoalalskd71@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: gimme some socks <bobteen1@gmail.com>
Co-authored-by: mahdi eslam panah <mahdii3375@gmail.com>
Co-authored-by: mateus zampol <mateuszampol2009@hotmail.it>
Co-authored-by: saturn <swagburritovg@gmail.com>
Co-authored-by: typek52 <typek52@gmail.com>
Co-authored-by: xmdb <klchiu721@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2022-03-26 12:38:56 -04:00
ffd9c6995a UpdatesController: Don't init adapter until chapter data is ready (#6824)
Considering there's no pagination for this list, the data loading can take some
time. So this will show the existing refresh indicator instead of empty view
while the list is loading.
2022-03-25 22:20:47 -04:00
ef600c0956 Fix extension update badge reset when app resumed (#6822) 2022-03-25 11:11:16 -04:00
5c0a43e8d6 Fix off by 1 dates (fixes #6791) 2022-03-24 18:49:08 -04:00
8e332dba30 Update Material Components 2022-03-24 18:44:48 -04:00
cd07027192 Use the file extension from the ImageType enum (#6800)
* Use the file extension from the ImageType enum

* Use the mime type from the ImageType enum

- On Android 29+
2022-03-21 13:13:39 -04:00
da2b30268a Add support for Happy Eyeballs 2022-03-19 16:48:33 -04:00
1163aa4e4e Share logic for saving page/cover (#6787)
* Use MediaStore on newer Android Q or newer

* Use flow instead of Observable

* Review comment fixes

* Use suspended function instead of flow
2022-03-19 16:46:23 -04:00
ddb856edc7 Add cover error drawable (#6782) 2022-03-15 22:21:30 -04:00
9c426bc216 Avoid crashing when global search encounters a NoClassDefFoundError 2022-03-15 22:20:41 -04:00
382852d0bd Require WebView v95+ 2022-03-15 22:12:41 -04:00
87ae86e1be Added reverse portrait reader rotation 2022-03-12 16:50:48 -05:00
9547311d7d Avoid throw as it is slow expensive operations 2022-03-12 16:47:31 -05:00
1613d561c1 Revert "Add shortcut to change app language in Android 13"
This reverts commit 538478cac8.
2022-03-12 16:45:36 -05:00
538478cac8 Add shortcut to change app language in Android 13 2022-03-11 22:26:03 -05:00
267ecce958 Support Android 13 themed app icon 2022-03-11 07:57:57 -05:00
fae43fedfa ReaderActivity: Reduce anim duration when launched from resume FAB (#6762)
From enter 500ms exit 400ms
To both 350ms
2022-03-10 07:51:42 -05:00
c447022092 Disable app cache WebView (is a deprecated web API and is being removed in Android 13) 2022-03-09 18:04:52 -05:00
56042ad0b6 Split out global library update skipped entries into separate notification (closes #6722) 2022-03-09 18:04:52 -05:00
45da036789 Avoid potentially deleting the entire backups folder 2022-03-09 18:04:52 -05:00
b47b702a52 Copy raw description on long tap (fixes #6557) 2022-03-09 18:04:52 -05:00
869424cd16 Change cover placeholder (#6756) 2022-03-09 17:26:55 -05:00
b9fd01315b Minor cleanup 2022-03-06 09:37:39 -05:00
a72098b862 Add shortcut to edit categories screen from category setting dialog (closes #6280) 2022-03-06 09:37:39 -05:00
86016de6cb Recreate Backup worker with IS_AUTO_BACKUP_KEY flag (#6742)
* Recreate Backup worker with IS_AUTO_BACKUP_KEY flag

* Extra safety net to not delete backup folder
2022-03-06 08:36:47 -05:00
592b9fedb9 Fixed the wrong offset (#6704) 2022-03-05 10:08:32 -05:00
d06984e3a3 Use same name for manual backup job tag and work name 2022-03-05 09:49:21 -05:00
6b55ee250d Update AGP and Gradle 2022-03-04 16:10:47 -05:00
10eef282fa Coil 2.x upgrade (#6725)
* Migrate to Coil 2

* Adapt to use coil disk cache

* Update to alpha 7

* Update to alpha 8

* Update to rc01
2022-03-04 16:04:32 -05:00
f312936629 Use Version Catalog & clean up Gradle files (#6728) 2022-03-04 09:58:31 -05:00
d53bb4c337 Use existing worker for manual backup creation (#6718)
* Use existing worker for manual backup creation

This will show the "creating backup" notification when auto backup is
running. Complete or error notification will continue to be shown only on
manual job.

* Make sure disabling auto backup don't cancel running manual backup job
2022-03-03 22:15:49 -05:00
1a605e27bc Remove unused string (#6726)
* change wording if update restriction is off

from
  Only update: none
to
  Restrictions: none

* remove unused string
2022-03-03 22:13:44 -05:00
08ee858f64 Adjust mark as unread and mark previous as read action visibility (#6703) 2022-03-01 22:21:15 -05:00
af70fe3e7e [skip ci] Move auto-closer rules 2022-02-27 14:50:48 -05:00
29c5c0af50 Update Material Components 2022-02-25 18:08:42 -05:00
9420b750d2 Adjust badge font weights 2022-02-25 18:08:29 -05:00
6f5328f663 Fix corrupted backup file, fix #6424 (#6691)
Reappear stably on the api30 Android Studio Emulator,
first save a large backup file,
then save a small backup file, overwriting the previous larger backup file,
so you get a backup file with a larger size but only the first part is meaningful,
2022-02-23 09:12:24 -05:00
90214d02d7 Add Prerequisites and Getting help to Contributing.md (#6682) 2022-02-22 07:58:00 -05:00
2f07f226b8 Fix "Landscape zoom" and "Navigate to pan" for split images (#6647)
* fix: getPageHolder would always return the first split, as they share the same index

* split pages have the same number, we need an extra check to know whether we move forward or back
2022-02-17 22:09:03 -05:00
a8ad19a89d Restore bottom nav position earlier after being recreated (#6648) 2022-02-17 22:08:36 -05:00
57c07250fd Side padding: Added missing percentage (#6668) 2022-02-17 10:39:07 -05:00
4a3e4a7c5c Reword library update restrictions setting and surface skipped entries in error notification/log 2022-02-14 18:16:22 -05:00
c284a23afb Avoid some crashes if router backstack is empty for whatever reason 2022-02-13 11:10:22 -05:00
fad1449de3 Grid items optimizations (#6641)
Use ConstraintLayout for ez size ratio calculation and merge cover-only view
holder with compact's
2022-02-13 11:09:49 -05:00
f18d161eaf Add "Started" library filter and library update restriction (#6382)
* Add chapter read count to library manga

Co-Authored-By: Jays2Kings <jays@outlook.com>

* Add "Started" library filter and library update restriction

* Update Filter when its changed

* Add back accidentally removed stuff.

* Update..

* Change variable names

* Change Variable name where I missed

Co-authored-by: Jays2Kings <jays@outlook.com>
2022-02-13 10:42:28 -05:00
88054b453a No need for a new bit for DisplayModeSetting mask
(Thanks Syer)
2022-02-12 22:26:51 -05:00
c560373596 Fix overlap between DisplayModeSetting and SortModeSetting masks 2022-02-12 22:17:33 -05:00
d698d03521 Fix Quad9 DoH setting 2022-02-12 22:08:12 -05:00
d8c8d7c588 Add Quad9 DOH provider (#6638)
* add quad9 as new doh provider

* add ipv6 addresses to google doh

* revert changes to import
2022-02-12 17:15:53 -05:00
9120e82517 Consistent divider colour 2022-02-12 13:24:20 -05:00
e214746536 Update action_display_cover_only_grid string 2022-02-12 13:15:19 -05:00
142396400c Weblate translations (#6537)
Co-authored-by: A <ville.mourujarvi@hostedweblate.mail.kapsi.fi>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Colin Tirion <grotehoed@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: KasukeLp <kasukelp23@yahoo.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Malek El Jubeily <malekjbeily@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Subha Das <subhadas68367@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
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/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/eu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sa/
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/th/
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: A <ville.mourujarvi@hostedweblate.mail.kapsi.fi>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Colin Tirion <grotehoed@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <alchemillatruth@purelymail.com>
Co-authored-by: Garutmaan Garuda <garutmaangaruda@gmail.com>
Co-authored-by: Giorgio Sanna <sannagiorgio1997@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: KasukeLp <kasukelp23@yahoo.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Malek El Jubeily <malekjbeily@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Ric <rikku.debec@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Subha Das <subhadas68367@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Unai <uesandi@gmail.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: stevenlele <stevenlele@outlook.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2022-02-12 13:14:23 -05:00
51d48bdde6 Update Theme Preview Items (#6628)
* Improved theme preview items

* Tweaked theme preference item border colours

* Polished theme items

* Update ThemesPreference.kt item layout width value

Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-02-12 13:14:04 -05:00
44b055c019 Cover only grid added to library (#6528)
* No title grid added to library and source

* Else added to display title in case image is null or empty

* No title grid renamed and now only available in library

* Spanish strings about cover only grid removed

Co-authored-by: micaelagimenez <micaela.gimenez@ext.prosegur.com>
2022-02-12 13:09:44 -05:00
790d7b9170 Rename extension function to avoid confusion with androidx function 2022-02-12 11:23:10 -05:00
d8719ceee9 Navigate to pan / landscape zoom (#6481)
* pan if the image is zoomed instead of navigating away
quickly display full landscape image before zooming to fit height in fit to screen

* add Tap to pan preference, defaults to true
add landscape zoom preference, defaults to false

* hide landscape image zoom option if scale is not fit screen

* fix landscape image zoom for first image and loading image

* properly reload pagerholders when landscape zoom option is changed

* enable landscape zoom by default
2022-02-12 11:21:54 -05:00
71ddb16574 Detect identical mangas when adding to library (#6579)
* added duplicate manga check

When adding a manga to your library, the app will go through each manga previously added and compare their names. If a match is detected, it will prompt the user and ask for confirmation. On this prompt there is also an option to view the other manga.

* added german translations for newly added strings

* Revert "added german translations for newly added strings"

This reverts commit 71ada620671651daeeb2546aecd02400a4bc86bc.

* changed `AlertDialog.Builder` to `MaterialAlertDialogBuilder`

* using SQL query instead of filtering entire library with Kotlin
2022-02-12 11:13:27 -05:00
2932ed670f MainActivity fixes (#6591)
* Reduce notifyDataSetChanged calls when category count is disabled

* Fix category tabs briefly showing when it's supposed to be disabled

Also fix tabs showing when activity recreated

* Lift appbar when tab is hidden

Check against tab visibility instead of viewpager

* Restore selected nav item after recreate

* Simplify SHORTCUT_MANGA intent handling

Don't need to change controller if the topmost controller is the target
2022-02-12 10:58:58 -05:00
ae2a6a3d4f Update dependencies 2022-02-12 10:11:03 -05:00
30061ada58 Update AGP for Android Studio Bumblebee | 2021.1.1 Patch 1 2022-02-12 10:09:30 -05:00
a131e28b60 [skip ci] docs: update app update checker link (#6619) 2022-02-10 08:51:08 -05:00
8c1662cfdb Disallow PackageInstaller extension installer option on MIUI 2022-02-05 23:02:13 -05:00
299e52e877 Allow disabling secure screen when incognito mode is on 2022-02-05 18:51:08 -05:00
95b253db09 Don't show error toasts in MangaController for HTTP 103 responses (closes #6562) 2022-02-05 18:26:50 -05:00
067cb2452e Add shortcut to backups guide 2022-02-05 17:44:54 -05:00
45e4092335 Increase minimum required disk space to download chapters to 200MB (closes #6576) 2022-02-05 17:35:54 -05:00
7659a997cf Update versions plugin 2022-02-05 17:27:36 -05:00
aa5e428222 Filter archive files as sequence 2022-02-05 17:27:28 -05:00
319e4360c8 Display correct string on FAB 2022-02-05 17:26:57 -05:00
f5c6e80dbb Add 5% webtoon reader side padding option (closes #6511) 2022-02-02 21:50:20 -05:00
7108993936 Unify reader error layout (#6512)
So nobody will think that the error layout is broken when they see different
layout.
2022-02-02 21:41:20 -05:00
b6553bdc34 ReaderActivity: Fix transition crash on Android 8 (#6542) 2022-02-02 21:40:48 -05:00
19fe689969 Revert "Temporarily revert some things for stable release"
This reverts commit b88f8ae9d2.
2022-02-01 12:32:27 -05:00
d6386cef41 Release v0.13.1 2022-02-01 12:32:21 -05:00
b88f8ae9d2 Temporarily revert some things for stable release 2022-02-01 12:31:47 -05:00
408c7b2ca6 Avoid unnecessary transition setup in reader if not transitioning 2022-02-01 10:31:44 -05:00
271253fd0b Fix app crashing when opening ReaderActivity with FAB (#6535) 2022-02-01 10:30:17 -05:00
5348154c42 TachiyomiAppBarLayout: Ignore inset visibility (#6533)
For resume button animation
2022-02-01 09:51:14 -05:00
e1b1f4f3fc Avoid trying to open links in invalid Huawei app 2022-02-01 09:48:37 -05:00
75a2110626 Update preference dependencies 2022-01-31 18:13:57 -05:00
9857d3d6ea Update issue templates 2022-01-31 16:10:41 -05:00
836a2649d3 Use default bottom nav height 2022-01-31 16:05:04 -05:00
59cba2533c Revert "Disable update/download warnings for stable release"
This reverts commit 3da8677e32.
2022-01-31 16:03:42 -05:00
444 changed files with 5957 additions and 3761 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.{kt,kts}]
indent_size=4
insert_final_newline=true
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true

View File

@ -3,7 +3,7 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.12.3)
- To the latest version of the app (stable is v0.13.2)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**.
placeholder: |
Example: "0.12.3"
Example: "0.13.2"
validations:
required: true
@ -98,7 +98,7 @@ body:
required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.13.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true

View File

@ -33,7 +33,7 @@ body:
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.13.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -2,5 +2,4 @@ org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2
kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process
kotlin.incremental=false

View File

@ -1,32 +0,0 @@
name: Issue closer
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: |
[
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
},
{
"type": "both",
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
"ignoreCase": true,
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
}
]

View File

@ -1,6 +1,8 @@
name: Issue moderator
on:
issues:
types: [opened, edited, reopened]
issue_comment:
types: [created]
@ -12,3 +14,22 @@ jobs:
uses: tachiyomiorg/issue-moderator-action@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
auto-close-rules: |
[
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
},
{
"type": "both",
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
"ignoreCase": true,
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
}
]

View File

@ -12,6 +12,21 @@ 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.
You do not need to ask for permission nor an assignment.
## Prerequisites
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
- Basic [Android development](https://developer.android.com/)
- [Kotlin](https://kotlinlang.org/)
### Tools
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled to test changes.
## Getting help
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
# Translations
@ -27,7 +42,7 @@ 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/GithubUpdateChecker.kt)
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.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:

View File

@ -1,8 +1,4 @@
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")
@ -13,7 +9,7 @@ plugins {
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply(plugin = "com.google.gms.google-services")
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
}
shortcutHelper.setFilePath("./shortcuts.xml")
@ -28,8 +24,8 @@ android {
applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 73
versionName = "0.13.0"
versionCode = 78
versionName = "0.13.2"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -137,153 +133,121 @@ android {
}
dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
implementation(kotlinx.reflect)
val coroutinesVersion = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
implementation(kotlinx.bundles.coroutines)
// Source models and interfaces from Tachiyomi 1.x
implementation("org.tachiyomi:source-api:1.1")
implementation(libs.tachiyomi.api)
// AndroidX libraries
implementation("androidx.annotation:annotation:1.4.0-alpha01")
implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
implementation("androidx.core:core-ktx:1.8.0-alpha02")
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
implementation(androidx.annotation)
implementation(androidx.appcompat)
implementation(androidx.biometricktx)
implementation(androidx.constraintlayout)
implementation(androidx.coordinatorlayout)
implementation(androidx.corektx)
implementation(androidx.splashscreen)
implementation(androidx.recyclerview)
implementation(androidx.swiperefreshlayout)
implementation(androidx.viewpager)
val lifecycleVersion = "2.4.0"
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
implementation(androidx.bundles.lifecycle)
// Job scheduling
implementation("androidx.work:work-runtime-ktx:2.6.0")
implementation(androidx.work.runtime)
// RX
implementation("io.reactivex:rxandroid:1.2.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
implementation(libs.bundles.reactivex)
implementation(libs.flowreactivenetwork)
// Network client
val okhttpVersion = "4.9.1"
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:3.0.0")
implementation(libs.bundles.okhttp)
implementation(libs.okio)
// TLS 1.3 support for Android < 10
implementation("org.conscrypt:conscrypt-android:2.5.2")
implementation(libs.conscrypt.android)
// Data serialization (JSON, protobuf)
val kotlinSerializationVersion = "1.3.2"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation(kotlinx.bundles.serialization)
// JavaScript engine
implementation("app.cash.quickjs:quickjs-android:0.9.2")
// TODO: remove Duktape once all extensions are using QuickJS
implementation("com.squareup.duktape:duktape-android:1.4.0")
implementation(libs.bundles.js.engine)
// HTML parser
implementation("org.jsoup:jsoup:1.14.3")
implementation(libs.jsoup)
// Disk
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.tachiyomiorg:unifile:17bec43")
implementation("com.github.junrar:junrar:7.4.0")
implementation(libs.disklrucache)
implementation(libs.unifile)
implementation(libs.junrar)
// Database
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
implementation(libs.bundles.sqlite)
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
implementation("com.github.requery:sqlite-android:3.36.0")
// Preferences
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
implementation(libs.preferencektx)
implementation(libs.flowpreferences)
// Model View Presenter
val nucleusVersion = "3.0.0"
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
implementation(libs.bundles.nucleus)
// Dependency injection
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation(libs.injekt.core)
// Image loading
val coilVersion = "1.4.0"
implementation("io.coil-kt:coil:$coilVersion")
implementation("io.coil-kt:coil-gif:$coilVersion")
implementation(libs.bundles.coil)
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
implementation(libs.subsamplingscaleimageview) {
exclude(module = "image-decoder")
}
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
implementation(libs.image.decoder)
// Sort
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
implementation(libs.natural.comparator)
// UI libraries
implementation("com.google.android.material:material:1.6.0-alpha02")
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533")
implementation("com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
implementation(libs.material)
implementation(libs.androidprocessbutton)
implementation(libs.flexible.adapter.core)
implementation(libs.flexible.adapter.ui)
implementation(libs.viewstatepageradapter)
implementation(libs.photoview)
implementation(libs.directionalviewpager) {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation("dev.chrisbanes.insetter:insetter:0.6.1")
implementation(libs.insetter)
// Conductor
val conductorVersion = "3.1.2"
implementation("com.bluelinelabs:conductor:$conductorVersion")
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
implementation(libs.bundles.conductor)
// FlowBinding
val flowbindingVersion = "1.2.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")
implementation(libs.bundles.flowbinding)
// Logging
implementation("com.squareup.logcat:logcat:0.1")
implementation(libs.logcat)
// Crash reports/analytics
implementation("ch.acra:acra-http:5.8.4")
"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
implementation(libs.acra.http)
"standardImplementation"(libs.firebase.analytics)
// Licenses
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
implementation(libs.aboutlibraries.core)
// Shizuku
val shizukuVersion = "12.1.0"
implementation("dev.rikka.shizuku:api:$shizukuVersion")
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
implementation(libs.bundles.shizuku)
// Tests
testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19")
testImplementation(libs.junit)
testImplementation(libs.assertj.core)
testImplementation(libs.mockito.core)
val robolectricVersion = "3.1.4"
testImplementation("org.robolectric:robolectric:$robolectricVersion")
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
testImplementation(libs.bundles.robolectric)
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
// debugImplementation(libs.leakcanary.android)
}
tasks {
@ -313,40 +277,8 @@ tasks {
}
}
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
classpath(kotlinx.gradle)
}
}
// 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,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
</adaptive-icon>

View File

@ -181,10 +181,6 @@
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />

View File

@ -20,17 +20,19 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.notification
@ -91,7 +93,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
PendingIntent.FLAG_ONE_SHOT
PendingIntent.FLAG_ONE_SHOT,
)
setContentIntent(pendingIntent)
}
@ -110,7 +112,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
},
)
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
@ -121,17 +123,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply {
componentRegistry {
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(this@App) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder(this@App))
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder())
add(GifDecoder.Factory())
}
add(TachiyomiImageDecoder(this@App.resources))
add(ByteBufferFetcher())
add(MangaCoverFetcher())
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverKeyer())
}
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
callFactory(callFactoryInit)
diskCache(diskCacheInit)
crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
if (preferences.verboseLogging()) logger(DebugLogger())
@ -144,6 +149,23 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}
}
override fun getPackageName(): String {
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Thread.currentThread().stackTrace
for (element in stackTrace) {
if ("org.chromium.base.BuildInfo".equals(element.className, ignoreCase = true)) {
if ("getAll".equals(element.methodName, ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
break
}
}
} catch (e: Exception) {
}
return super.getPackageName()
}
protected open fun setupAcra() {
if (BuildConfig.FLAVOR != "dev") {
initAcra {
@ -190,3 +212,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
/**
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
internal object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null
@Synchronized
fun get(context: Context): DiskCache {
return instance ?: run {
val safeCacheDir = context.cacheDir.apply { mkdirs() }
// Create the singleton disk cache instance.
DiskCache.Builder()
.directory(safeCacheDir.resolve(FOLDER_NAME))
.build()
.also { instance = it }
}
}
}

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.extension.ExtensionManager
@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
get<PreferencesHelper>()

View File

@ -5,8 +5,9 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
@ -18,6 +19,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt
@ -242,7 +244,26 @@ object Migrations {
if (oldVersion < 72) {
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) {
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
preferences.libraryUpdateMangaRestriction() -= MANGA_NON_COMPLETED
}
}
if (oldVersion < 75) {
val oldSecureScreen = prefs.getBoolean("secure_screen", false)
if (oldSecureScreen) {
preferences.secureScreen().set(PreferenceValues.SecureScreenMode.ALWAYS)
}
if (DeviceUtil.isMiui && preferences.extensionInstaller().get() == PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER) {
preferences.extensionInstaller().set(PreferenceValues.ExtensionInstaller.LEGACY)
}
}
if (oldVersion < 76) {
BackupCreatorJob.setupTask(context)
}
if (oldVersion < 77) {
val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) {
preferences.navigationModePager().set(5)
preferences.navigationModeWebtoon().set(5)
}
}

View File

@ -21,7 +21,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy()
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
/**
* Returns manga

View File

@ -112,7 +112,7 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
internal fun showRestoreProgress(
progress: Int,
amount: Int,
title: String
title: String,
) {
notifier.showRestoreProgress(title, progress, amount)
}

View File

@ -11,4 +11,15 @@ object BackupConst {
const val BACKUP_TYPE_LEGACY = 0
const val BACKUP_TYPE_FULL = 1
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
}

View File

@ -1,114 +0,0 @@
package eu.kanade.tachiyomi.data.backup
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
/**
* Service for backing up library information to a JSON file.
*/
class BackupCreateService : Service() {
companion object {
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupCreateService::class.java)
/**
* Make a backup from library
*
* @param context context of application
* @param uri path of Uri
* @param flags determines what to backup
*/
fun start(context: Context, uri: Uri, flags: Int) {
if (!isRunning(context)) {
val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_FLAGS, flags)
}
ContextCompat.startForegroundService(context, intent)
}
}
}
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: BackupNotifier
override fun onCreate() {
super.onCreate()
notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
super.onDestroy()
}
private fun destroyJob() {
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
try {
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}
stopSelf(startId)
return START_NOT_STICKY
}
}

View File

@ -1,15 +1,23 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.notificationManager
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -20,37 +28,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
val uri = preferences.backupsDirectory().get().toUri()
val flags = BackupCreateService.BACKUP_ALL
val notifier = BackupNotifier(context)
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
?: preferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
return try {
FullBackupManager(context).createBackup(uri, flags, true)
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup)
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
Result.success()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
if (!isAutoBackup) notifier.showBackupError(e.message)
Result.failure()
} finally {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
}
}
companion object {
private const val TAG = "BackupCreator"
fun isManualJobRunning(context: Context): Boolean {
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
return list.find { it.state == WorkInfo.State.RUNNING } != null
}
fun setupTask(context: Context, prefInterval: Int? = null) {
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().get()
val workManager = WorkManager.getInstance(context)
if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES
TimeUnit.MINUTES,
)
.addTag(TAG)
.addTag(TAG_AUTO)
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
workManager.cancelUniqueWork(TAG_AUTO)
}
}
fun startNow(context: Context, uri: Uri, flags: Int) {
val inputData = workDataOf(
IS_AUTO_BACKUP_KEY to false,
LOCATION_URI_KEY to uri.toString(),
BACKUP_FLAGS_KEY to flags,
)
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
.addTag(TAG_MANUAL)
.setInputData(inputData)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
}
}
}
private const val TAG_AUTO = "BackupCreator"
private const val TAG_MANUAL = "$TAG_AUTO:manual"
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
private const val LOCATION_URI_KEY = "location_uri" // String
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int

View File

@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
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, Notifications.ID_BACKUP_COMPLETE),
)
show(Notifications.ID_BACKUP_COMPLETE)
@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_stop),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
)
}
@ -124,8 +124,8 @@ class BackupNotifier(private val context: Context) {
R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time)
)
TimeUnit.MILLISECONDS.toMinutes(time),
),
)
with(completeNotificationBuilder) {

View File

@ -4,14 +4,14 @@ import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
@ -32,6 +32,7 @@ import logcat.LogPriority
import okio.buffer
import okio.gzip
import okio.sink
import java.io.FileOutputStream
import kotlin.math.max
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
@ -42,9 +43,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* Create backup Json file from database
*
* @param uri path of Uri
* @param isJob backup called from job
* @param isAutoBackup backup called from scheduled backup job
*/
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String {
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
// Create root object
var backup: Backup? = null
@ -55,14 +56,14 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backupManga(databaseManga, flags),
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga)
backupExtensionInfo(databaseManga),
)
}
var file: UniFile? = null
try {
file = (
if (isJob) {
if (isAutoBackup) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
@ -84,8 +85,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
)
?: throw Exception("Couldn't create backup file")
if (!file.isFile) {
throw IllegalStateException("Failed to get handle on file")
}
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
file.openOutputStream().also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
}.sink().gzip().buffer().use { it.write(byteArray) }
val fileUri = file.uri
// Make sure it's a valid backup file

View File

@ -93,7 +93,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>
backupCategories: List<BackupCategory>,
) {
db.inTransaction {
val dbManga = backupManager.getMangaFromDatabase(manga)
@ -123,7 +123,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>
backupCategories: List<BackupCategory>,
) {
try {
val fetchedManga = backupManager.restoreManga(manga)
@ -143,7 +143,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>
backupCategories: List<BackupCategory>,
) {
backupManager.restoreChaptersForManga(backupManga, chapters)

View File

@ -9,5 +9,5 @@ data class Backup(
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
)

View File

@ -26,7 +26,7 @@ class BackupCategory(
return BackupCategory(
name = category.name,
order = category.order,
flags = category.flags
flags = category.flags,
)
}
}

View File

@ -49,7 +49,7 @@ data class BackupChapter(
lastPageRead = chapter.last_page_read,
dateFetch = chapter.date_fetch,
dateUpload = chapter.date_upload,
sourceOrder = chapter.source_order
sourceOrder = chapter.source_order,
)
}
}

View File

@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long
@ProtoNumber(1) var lastRead: Long,
)
@Serializable
data class BackupHistory(
@ProtoNumber(1) var url: String,
@ProtoNumber(2) var lastRead: Long
@ProtoNumber(2) var lastRead: Long,
)

View File

@ -35,7 +35,7 @@ data class BackupManga(
@ProtoNumber(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList()
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
@ -83,7 +83,7 @@ data class BackupManga(
dateAdded = manga.date_added,
viewer = manga.readingModeType,
viewer_flags = manga.viewer_flags,
chapterFlags = manga.chapter_flags
chapterFlags = manga.chapter_flags,
)
}
}

View File

@ -7,19 +7,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long
@ProtoNumber(1) var sourceId: Long,
)
@Serializable
data class BackupSource(
@ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long
@ProtoNumber(2) var sourceId: Long,
) {
companion object {
fun copyFrom(source: Source): BackupSource {
return BackupSource(
name = source.name,
sourceId = source.id
sourceId = source.id,
)
}
}

View File

@ -56,7 +56,7 @@ data class BackupTracking(
status = track.status,
startedReadingDate = track.started_reading_date,
finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url
trackingUrl = track.tracking_url,
)
}
}

View File

@ -55,9 +55,9 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
* Create backup Json file from database
*
* @param uri path of Uri
* @param isJob backup called from job
* @param isAutoBackup backup called from scheduled backup job
*/
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean) =
throw IllegalStateException("Legacy backup creation is not supported")
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {

View File

@ -28,7 +28,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
// Read the json and create a Json Object,
// cannot use the backupManager json deserializer one because its not initialized yet
val backupObject = Json.decodeFromStream<JsonObject>(
context.contentResolver.openInputStream(uri)!!
context.contentResolver.openInputStream(uri)!!,
)
// Get parser version
@ -109,7 +109,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
tracks: List<Track>,
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
@ -139,7 +139,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
tracks: List<Track>,
) {
try {
val fetchedManga = backupManager.fetchManga(source, manga)
@ -161,7 +161,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
tracks: List<Track>,
) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)

View File

@ -21,7 +21,7 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
val backup = try {
backupManager.parser.decodeFromStream<Backup>(
context.contentResolver.openInputStream(uri)!!
context.contentResolver.openInputStream(uri)!!,
)
} catch (e: Exception) {
throw ValidatorParseException(e)

View File

@ -15,7 +15,7 @@ data class Backup(
val version: Int? = null,
var mangas: MutableList<MangaObject> = mutableListOf(),
var categories: List<@Contextual Category>? = null,
var extensions: List<String>? = null
var extensions: List<String>? = null,
) {
companion object {
const val CURRENT_VERSION = 2
@ -33,5 +33,5 @@ data class MangaObject(
var chapters: List<@Contextual Chapter>? = null,
var categories: List<String>? = null,
var track: List<@Contextual Track>? = null,
var history: List<@Contextual DHistory>? = null
var history: List<@Contextual DHistory>? = null,
)

View File

@ -27,7 +27,7 @@ open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
buildJsonArray {
add(value.name)
add(value.order)
}
},
)
}

View File

@ -35,7 +35,7 @@ open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
if (value.last_page_read != 0) {
put(LAST_READ, value.last_page_read)
}
}
},
)
}

View File

@ -26,7 +26,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
buildJsonArray {
add(value.url)
add(value.lastRead)
}
},
)
}
@ -35,7 +35,7 @@ object HistoryTypeSerializer : KSerializer<DHistory> {
val array = decoder.decodeJsonElement().jsonArray
return DHistory(
url = array[0].jsonPrimitive.content,
lastRead = array[1].jsonPrimitive.long
lastRead = array[1].jsonPrimitive.long,
)
}
}

View File

@ -31,7 +31,7 @@ open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
add(value.source)
add(value.viewer_flags)
add(value.chapter_flags)
}
},
)
}

View File

@ -33,7 +33,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> {
put(LIBRARY, value.library_id)
put(LAST_READ, value.last_chapter_read)
put(TRACKING_URL, value.tracking_url)
}
},
)
}

View File

@ -49,7 +49,7 @@ class ChapterCache(private val context: Context) {
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE
PARAMETER_CACHE_SIZE,
)
/**

View File

@ -104,7 +104,7 @@ class CoverCache(private val context: Context) {
* Clear coil's memory cache.
*/
fun clearMemoryCache() {
context.imageLoader.memoryCache.clear()
context.imageLoader.memoryCache?.clear()
}
private fun getCacheDir(dir: String): File {

View File

@ -1,25 +0,0 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okio.buffer
import okio.source
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
class ByteBufferFetcher : Fetcher<ByteBuffer> {
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
return SourceResult(
source = ByteArrayInputStream(data.array()).source().buffer(),
mimeType = null,
dataSource = DataSource.MEMORY
)
}
override fun key(data: ByteBuffer): String? = null
}

View File

@ -1,149 +1,261 @@
package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.Options
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.network.HttpException
import coil.request.get
import coil.size.Size
import coil.request.Options
import coil.request.Parameters
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okio.Path.Companion.toOkioPath
import okio.Source
import okio.buffer
import okio.sink
import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.HttpURLConnection
/**
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
* A [Fetcher] that fetches cover image for [Manga] object.
*
* It uses [Manga.thumbnail_url] if custom cover is not set by the user.
* Disk caching for library items is handled by [CoverCache], otherwise
* handled by Coil's [DiskCache].
*
* Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
*/
class MangaCoverFetcher : Fetcher<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
class MangaCoverFetcher(
private val manga: Manga,
private val sourceLazy: Lazy<HttpSource?>,
private val options: Options,
private val coverCache: CoverCache,
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher {
override fun key(data: Manga): String? {
if (data.thumbnail_url.isNullOrBlank()) return null
return data.thumbnail_url!!
}
// For non-custom cover
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
private lateinit var url: String
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
override suspend fun fetch(): FetchResult {
// Use custom cover if exists
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
val customCoverFile = coverCache.getCustomCoverFile(data)
val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
val customCoverFile = coverCache.getCustomCoverFile(manga)
if (useCustomCover && customCoverFile.exists()) {
return fileLoader(customCoverFile)
}
val cover = data.thumbnail_url
return when (getResourceType(cover)) {
Type.URL -> httpLoader(data, options)
Type.File -> fileLoader(data)
// diskCacheKey is thumbnail_url
url = diskCacheKey ?: error("No cover specified")
return when (getResourceType(url)) {
Type.URL -> httpLoader()
Type.File -> fileLoader(File(url.substringAfter("file://")))
null -> error("Invalid image")
}
}
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
}
private suspend fun httpLoader(): FetchResult {
// Only cache separately if it's a library item
val coverCacheFile = if (manga.favorite) {
val libraryCoverCacheFile = if (manga.favorite) {
coverCache.getCoverFile(manga) ?: error("No cover specified")
} else {
null
}
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
return fileLoader(coverCacheFile)
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
return fileLoader(libraryCoverCacheFile)
}
val (response, body) = awaitGetCall(manga, options)
if (!response.isSuccessful) {
body.close()
var snapshot = readFromDiskCache()
try {
// Fetch from disk cache
if (snapshot != null) {
val snapshotCoverCache = moveSnapshotToCoverCache(snapshot, libraryCoverCacheFile)
if (snapshotCoverCache != null) {
// Read from cover cache after added to library
return fileLoader(snapshotCoverCache)
}
// Read from snapshot
return SourceResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
}
// Fetch from network
val response = executeNetworkRequest()
val responseBody = checkNotNull(response.body) { "Null response source" }
try {
// Read from cover cache after library manga cover updated
val responseCoverCache = writeResponseToCoverCache(response, libraryCoverCacheFile)
if (responseCoverCache != null) {
return fileLoader(responseCoverCache)
}
// Read from disk cache
snapshot = writeToDiskCache(snapshot, response)
if (snapshot != null) {
return SourceResult(
source = snapshot.toImageSource(),
mimeType = "image/*",
dataSource = DataSource.NETWORK,
)
}
// Read from response if cache is unused or unusable
return SourceResult(
source = ImageSource(source = responseBody.source(), context = options.context),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK,
)
} catch (e: Exception) {
responseBody.closeQuietly()
throw e
}
} catch (e: Exception) {
snapshot?.closeQuietly()
throw e
}
}
private suspend fun executeNetworkRequest(): Response {
val client = sourceLazy.value?.client ?: callFactoryLazy.value
val response = client.newCall(newRequest()).await()
if (!response.isSuccessful && response.code != HttpURLConnection.HTTP_NOT_MODIFIED) {
response.body?.closeQuietly()
throw HttpException(response)
}
return response
}
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
@Suppress("BlockingMethodInNonBlockingContext")
response.peekBody(Long.MAX_VALUE).source().use { input ->
coverCacheFile.parentFile?.mkdirs()
if (coverCacheFile.exists()) {
coverCacheFile.delete()
}
coverCacheFile.sink().buffer().use { output ->
output.writeAll(input)
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
}
return SourceResult(
source = body.source(),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
return request.build()
}
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
val call = getCall(manga, options)
val response = call.await()
return response to checkNotNull(response.body) { "Null response source" }
}
private fun getCall(manga: Manga, options: Options): Call {
val source = sourceManager.get(manga.source) as? HttpSource
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
if (cacheFile == null) return null
return try {
diskCacheLazy.value.run {
fileSystem.source(snapshot.data).use { input ->
writeSourceToCoverCache(input, cacheFile)
}
remove(diskCacheKey!!)
}
cacheFile.takeIf { it.exists() }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to write snapshot data to cover cache ${cacheFile.name}" }
null
}
}
val networkRead = options.networkCachePolicy.readEnabled
val diskRead = options.diskCachePolicy.readEnabled
when {
!networkRead && diskRead -> {
it.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
it.cacheControl(CacheControl.FORCE_NETWORK)
} else {
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
}
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
return try {
response.peekBody(Long.MAX_VALUE).source().use { input ->
writeSourceToCoverCache(input, cacheFile)
}
}.build()
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
return client.newCall(request)
cacheFile.takeIf { it.exists() }
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
null
}
}
private fun fileLoader(manga: Manga): FetchResult {
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
cacheFile.parentFile?.mkdirs()
cacheFile.delete()
try {
cacheFile.sink().buffer().use { output ->
output.writeAll(input)
}
} catch (e: Exception) {
cacheFile.delete()
throw e
}
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
source = file.source().buffer(),
mimeType = "image/*",
dataSource = DataSource.DISK
)
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
}
private fun writeToDiskCache(
snapshot: DiskCache.Snapshot?,
response: Response,
): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled) {
snapshot?.closeQuietly()
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
}
return editor.commitAndGet()
} catch (e: Exception) {
try {
editor.abort()
} catch (ignored: Exception) {
}
throw e
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
}
private fun getResourceType(cover: String?): Type? {
@ -159,6 +271,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
File, URL
}
class Factory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
val source = lazy { sourceManager.get(data.source) as? HttpSource }
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
}
}
companion object {
const val USE_CUSTOM_COVER = "use_custom_cover"

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.coil
import coil.key.Keyer
import coil.request.Options
import eu.kanade.tachiyomi.data.database.models.Manga
class MangaCoverKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String? {
return data.thumbnail_url?.takeIf { it.isNotBlank() }
}
}

View File

@ -1,13 +1,14 @@
package eu.kanade.tachiyomi.data.coil
import android.content.res.Resources
import android.os.Build
import androidx.core.graphics.drawable.toDrawable
import coil.bitmap.BitmapPool
import coil.ImageLoader
import coil.decode.DecodeResult
import coil.decode.Decoder
import coil.decode.Options
import coil.size.Size
import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource
import tachiyomi.decoder.ImageDecoder
@ -15,26 +16,10 @@ import tachiyomi.decoder.ImageDecoder
/**
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
*/
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
val type = source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false
}
}
override suspend fun decode(
pool: BitmapPool,
source: BufferedSource,
size: Size,
options: Options
): DecodeResult {
val decoder = source.use {
override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.use {
ImageDecoder.newInstance(it.inputStream())
}
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
check(bitmap != null) { "Failed to decode image." }
return DecodeResult(
drawable = bitmap.toDrawable(resources),
isSampled = false
drawable = bitmap.toDrawable(options.context.resources),
isSampled = false,
)
}
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
}
private fun isApplicable(source: BufferedSource): Boolean {
val type = source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
else -> false
}
}
override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory
override fun hashCode() = javaClass.hashCode()
}
}

View File

@ -46,7 +46,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
// Fix kissmanga covers after supporting cloudflare
db.execSQL(
"""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4"""
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""",
)
}
if (oldVersion < 3) {

View File

@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(),
CategoryGetResolver(),
CategoryDeleteResolver()
CategoryDeleteResolver(),
)
class CategoryPutResolver : DefaultPutResolver<Category>() {
@ -40,7 +40,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
COL_ID to obj.id,
COL_NAME to obj.name,
COL_ORDER to obj.order,
COL_FLAGS to obj.flags
COL_FLAGS to obj.flags,
)
}

View File

@ -28,7 +28,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(),
ChapterGetResolver(),
ChapterDeleteResolver()
ChapterDeleteResolver(),
)
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
@ -56,7 +56,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
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
COL_SOURCE_ORDER to obj.source_order,
)
}

View File

@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver()
HistoryDeleteResolver(),
)
open class HistoryPutResolver : DefaultPutResolver<History>() {
@ -40,7 +40,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
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
COL_TIME_READ to obj.time_read,
)
}

View File

@ -18,7 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(),
MangaCategoryGetResolver(),
MangaCategoryDeleteResolver()
MangaCategoryDeleteResolver(),
)
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
@ -37,7 +37,7 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_CATEGORY_ID to obj.category_id
COL_CATEGORY_ID to obj.category_id,
)
}

View File

@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(),
MangaGetResolver(),
MangaDeleteResolver()
MangaDeleteResolver(),
)
class MangaPutResolver : DefaultPutResolver<Manga>() {
@ -66,7 +66,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
COL_DATE_ADDED to obj.date_added
COL_DATE_ADDED to obj.date_added,
)
}

View File

@ -29,7 +29,7 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver()
TrackDeleteResolver(),
)
class TrackPutResolver : DefaultPutResolver<Track>() {
@ -58,7 +58,7 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
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
COL_FINISH_DATE to obj.finished_reading_date,
)
}

View File

@ -2,7 +2,14 @@ package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var unreadCount: Int = 0
var readCount: Int = 0
val totalChapters
get() = readCount + unreadCount
val hasStarted
get() = readCount > 0
var category: Int = 0
}

View File

@ -125,6 +125,6 @@ fun Manga.toMangaInfo(): MangaInfo {
genres = this.getGenres() ?: emptyList(),
key = this.url,
status = this.status,
title = this.title
title = this.title,
)
}

View File

@ -15,7 +15,7 @@ interface CategoryQueries : DbProvider {
Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build()
.build(),
)
.prepare()
@ -25,7 +25,7 @@ interface CategoryQueries : DbProvider {
RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build()
.build(),
)
.prepare()

View File

@ -23,7 +23,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build()
.build(),
)
.prepare()
@ -34,7 +34,7 @@ interface ChapterQueries : DbProvider {
.query(getRecentsQuery())
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build()
.build(),
)
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
@ -46,7 +46,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(id)
.build()
.build(),
)
.prepare()
@ -57,7 +57,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build()
.build(),
)
.prepare()
@ -68,7 +68,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(url, mangaId)
.build()
.build(),
)
.prepare()

View File

@ -32,7 +32,7 @@ interface HistoryQueries : DbProvider {
.query(getRecentMangasQuery(search))
.args(date.time, limit, offset)
.observesTables(HistoryTable.TABLE)
.build()
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
@ -44,7 +44,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build()
.build(),
)
.prepare()
@ -55,7 +55,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build()
.build(),
)
.prepare()
@ -83,7 +83,7 @@ interface HistoryQueries : DbProvider {
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.build()
.build(),
)
.prepare()
@ -93,7 +93,7 @@ interface HistoryQueries : DbProvider {
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build()
.build(),
)
.prepare()
}

View File

@ -20,7 +20,7 @@ interface MangaCategoryQueries : DbProvider {
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build()
.build(),
)
.prepare()

View File

@ -29,11 +29,26 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build()
.build(),
)
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getDuplicateLibraryManga(manga: Manga) = db.get()
.`object`(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
.whereArgs(
manga.title.lowercase(),
manga.source,
)
.limit(1)
.build(),
)
.prepare()
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
var queryBuilder = Query.builder()
.table(MangaTable.TABLE)
@ -57,7 +72,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId)
.build()
.build(),
)
.prepare()
@ -68,7 +83,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build()
.build(),
)
.prepare()
@ -78,7 +93,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(getSourceIdsWithNonLibraryMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
.build(),
)
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
.prepare()
@ -137,7 +152,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
.whereArgs(0, *sourceIds.toTypedArray())
.build()
.build(),
)
.prepare()
@ -145,7 +160,7 @@ interface MangaQueries : DbProvider {
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.build()
.build(),
)
.prepare()
@ -155,7 +170,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
.build(),
)
.prepare()
@ -165,7 +180,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(getTotalChapterMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
.build(),
)
.prepare()
@ -175,7 +190,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(getLatestChapterMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
.build(),
)
.prepare()
@ -185,7 +200,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
.build(),
)
.prepare()
}

View File

@ -8,21 +8,28 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCateg
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
* Query to get the manga from the library, with their categories, read and unread count.
*/
val libraryQuery =
"""
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID}
) AS R
ON ${Manga.COL_ID} = R.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}

View File

@ -15,7 +15,7 @@ interface TrackQueries : DbProvider {
.withQuery(
Query.builder()
.table(TrackTable.TABLE)
.build()
.build(),
)
.prepare()
@ -26,7 +26,7 @@ interface TrackQueries : DbProvider {
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build()
.build(),
)
.prepare()
@ -40,7 +40,7 @@ interface TrackQueries : DbProvider {
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build()
.build(),
)
.prepare()
}

View File

@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
)
}

View File

@ -29,6 +29,6 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
)
}

View File

@ -29,6 +29,6 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read,
)
}

View File

@ -27,6 +27,6 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_SOURCE_ORDER to chapter.source_order
ChapterTable.COL_SOURCE_ORDER to chapter.source_order,
)
}

View File

@ -24,7 +24,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build()
.build(),
)
cursor.use { putCursor ->
@ -47,6 +47,6 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
private fun mapToUpdateContentValues(history: History) =
contentValuesOf(
HistoryTable.COL_LAST_READ to history.last_read
HistoryTable.COL_LAST_READ to history.last_read,
)
}

View File

@ -16,8 +16,9 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
val manga = LibraryManga()
mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
manga.unreadCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_UNREAD_COUNT))
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
return manga
}

View File

@ -27,6 +27,6 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified
MangaTable.COL_COVER_LAST_MODIFIED to manga.cover_last_modified,
)
}

View File

@ -27,6 +27,6 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_FAVORITE to manga.favorite
MangaTable.COL_FAVORITE to manga.favorite,
)
}

View File

@ -28,6 +28,6 @@ class MangaFlagsPutResolver(private val colName: String, private val fieldGetter
fun mapToContentValues(manga: Manga) =
contentValuesOf(
colName to fieldGetter.get(manga)
colName to fieldGetter.get(manga),
)
}

View File

@ -27,6 +27,6 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_LAST_UPDATE to manga.last_update
MangaTable.COL_LAST_UPDATE to manga.last_update,
)
}

View File

@ -27,6 +27,6 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_TITLE to manga.title
MangaTable.COL_TITLE to manga.title,
)
}

View File

@ -39,12 +39,15 @@ object MangaTable {
const val COL_CHAPTER_FLAGS = "chapter_flags"
const val COL_UNREAD = "unread"
const val COL_CATEGORY = "category"
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
// Not an actual value but computed when created
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
const val COMPUTED_COL_READ_COUNT = "read_count"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(

View File

@ -73,7 +73,7 @@ object TrackTable {
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|FROM ${TABLE}_tmp
""".trimMargin()
""".trimMargin()
val dropTempTable: String
get() = "DROP TABLE ${TABLE}_tmp"

View File

@ -27,7 +27,7 @@ class DownloadCache(
private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager,
private val preferences: PreferencesHelper = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get(),
) {
/**
@ -236,7 +236,7 @@ class DownloadCache(
*/
private class RootDirectory(
val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf()
var files: Map<Long, SourceDirectory> = hashMapOf(),
)
/**
@ -244,7 +244,7 @@ class DownloadCache(
*/
private class SourceDirectory(
val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf()
var files: Map<String, MangaDirectory> = hashMapOf(),
)
/**
@ -252,7 +252,7 @@ class DownloadCache(
*/
private class MangaDirectory(
val dir: UniFile,
var files: Set<String> = hashSetOf()
var files: Set<String> = hashSetOf(),
)
/**

View File

@ -30,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
*/
class DownloadManager(
private val context: Context,
private val db: DatabaseHelper = Injekt.get()
private val db: DatabaseHelper = Injekt.get(),
) {
private val sourceManager: SourceManager by injectLazy()

View File

@ -93,14 +93,14 @@ internal class DownloadNotifier(private val context: Context) {
addAction(
R.drawable.ic_pause_24dp,
context.getString(R.string.action_pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context)
NotificationReceiver.pauseDownloadsPendingBroadcast(context),
)
}
val downloadingProgressText = context.getString(
R.string.chapter_downloading_progress,
download.downloadedImages,
download.pages!!.size
download.pages!!.size,
)
if (preferences.hideNotificationContent()) {
@ -138,13 +138,13 @@ internal class DownloadNotifier(private val context: Context) {
addAction(
R.drawable.ic_play_arrow_24dp,
context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context)
NotificationReceiver.resumeDownloadsPendingBroadcast(context),
)
// Clear action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
NotificationReceiver.clearDownloadsPendingBroadcast(context),
)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
@ -213,7 +213,7 @@ internal class DownloadNotifier(private val context: Context) {
// Create notification
with(errorNotificationBuilder) {
setContentTitle(
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title),
)
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(R.drawable.ic_warning_white_24dp)

View File

@ -126,7 +126,7 @@ class DownloadPendingDeleter(context: Context) {
@Serializable
private data class Entry(
val chapters: List<ChapterEntry>,
val manga: MangaEntry
val manga: MangaEntry,
)
/**
@ -137,7 +137,7 @@ class DownloadPendingDeleter(context: Context) {
val id: Long,
val url: String,
val name: String,
val scanlator: String? = null
val scanlator: String? = null,
)
/**
@ -148,7 +148,7 @@ class DownloadPendingDeleter(context: Context) {
val id: Long,
val url: String,
val title: String,
val source: Long
val source: Long,
)
/**

View File

@ -138,7 +138,7 @@ class DownloadProvider(private val context: Context) {
when {
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
else -> chapter.name
}
},
)
}
@ -157,7 +157,7 @@ class DownloadProvider(private val context: Context) {
"$chapterName.cbz",
// Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name)
DiskUtil.buildValidFilename(chapter.name),
)
}
}

View File

@ -177,7 +177,9 @@ class DownloadService : Service() {
*/
private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay
.doOnError { /* Swallow wakelock error */ }
.doOnError {
/* Swallow wakelock error */
}
.subscribe { running ->
if (running) {
wakeLock.acquireIfNeeded()

View File

@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
*/
class DownloadStore(
context: Context,
private val sourceManager: SourceManager
private val sourceManager: SourceManager,
) {
/**

View File

@ -2,9 +2,11 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.webkit.MimeTypeMap
import android.widget.Toast
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter
@ -21,10 +23,12 @@ 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.plusAssign
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async
import logcat.LogPriority
import okhttp3.Response
@ -57,7 +61,7 @@ class Downloader(
private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache,
private val sourceManager: SourceManager
private val sourceManager: SourceManager,
) {
private val chapterCache: ChapterCache by injectLazy()
@ -208,7 +212,7 @@ class Downloader(
downloadChapter(download).subscribeOn(Schedulers.io())
}
},
5
5,
)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
@ -220,7 +224,7 @@ class Downloader(
DownloadService.stop(context)
logcat(LogPriority.ERROR, error)
notifier.onError(error.message)
}
},
)
}
@ -275,11 +279,11 @@ class Downloader(
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size }
// TODO: re-enable warning
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
// withUIContext {
// context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)
// }
// TODO: show warnings in stable
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD && BuildConfig.FLAVOR != "stable") {
withUIContext {
context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG)
}
}
DownloadService.start(context)
}
@ -476,7 +480,7 @@ class Downloader(
download: Download,
mangaDir: UniFile,
tmpDir: UniFile,
dirname: String
dirname: String,
) {
// Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -561,5 +565,5 @@ class Downloader(
}
}
// Arbitrary minimum required space to start a download: 50 MB
private const val MIN_DISK_SPACE = 50 * 1024 * 1024
// Arbitrary minimum required space to start a download: 200 MB
private const val MIN_DISK_SPACE = 200L * 1024 * 1024

View File

@ -11,7 +11,7 @@ import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(
private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList()
private val queue: MutableList<Download> = CopyOnWriteArrayList(),
) : List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()

View File

@ -49,7 +49,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES
TimeUnit.MINUTES,
)
.addTag(TAG)
.setConstraints(constraints)

View File

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.Downloader
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -85,31 +86,52 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder
.setProgress(total, current, false)
.build()
.build(),
)
}
/**
* Shows notification containing update entries that failed with action to open full log.
*
* @param errors List of entry titles that failed to update.
* @param failed Number of entries that failed to update.
* @param uri Uri for error log file containing all titles that failed.
*/
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
if (errors.isEmpty()) {
fun showUpdateErrorNotification(failed: Int, uri: Uri) {
if (failed == 0) {
return
}
context.notificationManager.notify(
Notifications.ID_LIBRARY_ERROR,
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
setContentTitle(context.resources.getString(R.string.notification_update_error, failed))
setContentText(context.getString(R.string.action_show_errors))
setSmallIcon(R.drawable.ic_tachi)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri))
}
.build()
.build(),
)
}
/**
* Shows notification containing update entries that were skipped.
*
* @param skipped Number of entries that were skipped during the update.
*/
fun showUpdateSkippedNotification(skipped: Int) {
if (skipped == 0) {
return
}
context.notificationManager.notify(
Notifications.ID_LIBRARY_SKIPPED,
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_SKIPPED) {
setContentTitle(context.resources.getString(R.string.notification_update_skipped, skipped))
setSmallIcon(R.drawable.ic_tachi)
addAction(R.drawable.ic_help_24dp, context.getString(R.string.learn_more), NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
}
.build(),
)
}
@ -139,8 +161,8 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") {
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
}
)
},
),
)
}
}
@ -155,7 +177,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(getNotificationIntent())
setAutoCancel(true)
}
},
)
// Per-manga notification
@ -200,8 +222,8 @@ class LibraryUpdateNotifier(private val context: Context) {
context,
manga,
chapters,
Notifications.ID_NEW_CHAPTERS
)
Notifications.ID_NEW_CHAPTERS,
),
)
// View chapters action
addAction(
@ -210,8 +232,8 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationReceiver.openChapterPendingActivity(
context,
manga,
Notifications.ID_NEW_CHAPTERS
)
Notifications.ID_NEW_CHAPTERS,
),
)
// Download chapters action
// Only add the action when chapters is within threshold
@ -223,8 +245,8 @@ class LibraryUpdateNotifier(private val context: Context) {
context,
manga,
chapters,
Notifications.ID_NEW_CHAPTERS
)
Notifications.ID_NEW_CHAPTERS,
),
)
}
}
@ -251,7 +273,7 @@ class LibraryUpdateNotifier(private val context: Context) {
val formatter = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' }
.apply { decimalSeparator = '.' },
)
val displayableChapterNumbers = chapters
@ -303,10 +325,9 @@ class LibraryUpdateNotifier(private val context: Context) {
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
}
}
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
private const val HELP_SKIPPED_URL = "https://tachiyomi.org/help/faq/#why-does-global-update-skip-some-entries"

View File

@ -5,7 +5,9 @@ import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import android.widget.Toast
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -18,8 +20,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
@ -40,6 +43,7 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -74,7 +78,7 @@ class LibraryUpdateService(
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get(),
val coverCache: CoverCache = Injekt.get()
val coverCache: CoverCache = Injekt.get(),
) : Service() {
private lateinit var wakeLock: PowerManager.WakeLock
@ -138,7 +142,7 @@ class LibraryUpdateService(
true
} else {
instance?.addMangaToQueue(category?.id ?: -1, target)
instance?.addMangaToQueue(category?.id ?: -1)
false
}
}
@ -210,7 +214,7 @@ class LibraryUpdateService(
// Update favorite manga
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
addMangaToQueue(categoryId, target)
addMangaToQueue(categoryId)
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception ->
@ -235,10 +239,10 @@ class LibraryUpdateService(
* @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update.
*/
fun addMangaToQueue(categoryId: Int, target: Target) {
fun addMangaToQueue(categoryId: Int) {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
var listToUpdate = if (categoryId != -1) {
val listToUpdate = if (categoryId != -1) {
libraryManga.filter { it.category == categoryId }
} else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
@ -258,16 +262,6 @@ class LibraryUpdateService(
listToInclude.minus(listToExclude)
}
if (target == Target.CHAPTERS) {
val restrictions = preferences.libraryUpdateMangaRestriction().get()
if (MANGA_ONGOING in restrictions) {
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
}
if (MANGA_FULLY_READ in restrictions) {
listToUpdate = listToUpdate.filter { it.unread == 0 }
}
}
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedBy { it.title }
@ -277,9 +271,9 @@ class LibraryUpdateService(
.groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
// TODO: re-enable warning
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
// toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
// TODO: show warnings in stable
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD && BuildConfig.FLAVOR != "stable") {
toast(R.string.notification_size_warning, Toast.LENGTH_LONG)
}
}
@ -297,10 +291,12 @@ class LibraryUpdateService(
val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false)
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
val restrictions = preferences.libraryUpdateMangaRestriction().get()
withIOContext {
mangaToUpdate.groupBy { it.source }
@ -319,19 +315,33 @@ class LibraryUpdateService(
manga,
) { manga ->
try {
val (newChapters, _) = updateManga(manga)
if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads.set(true)
when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
}
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
}
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
}
else -> {
// Convert to the manga that contains new chapters
val (newChapters, _) = updateManga(manga)
// Convert to the manga that contains new chapters
newUpdates.add(
manga to newChapters.sortedByDescending { ch -> ch.source_order }
.toTypedArray()
)
if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads.set(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 = when (e) {
@ -374,10 +384,13 @@ class LibraryUpdateService(
if (failedUpdates.isNotEmpty()) {
val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title },
errorFile.getUriCompat(this)
failedUpdates.size,
errorFile.getUriCompat(this),
)
}
if (skippedUpdates.isNotEmpty()) {
notifier.showUpdateSkippedNotification(skippedUpdates.size)
}
}
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -525,7 +538,7 @@ class LibraryUpdateService(
notifier.showProgressNotification(
updatingManga,
completed.get(),
mangaToUpdate.size
mangaToUpdate.size,
)
block(manga)
@ -539,7 +552,7 @@ class LibraryUpdateService(
notifier.showProgressNotification(
updatingManga,
completed.get(),
mangaToUpdate.size
mangaToUpdate.size,
)
}

View File

@ -6,8 +6,6 @@ import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.storage.getUriCompat
import java.io.File
/**
* Class that manages [PendingIntent] of activity's
@ -23,7 +21,7 @@ object NotificationHandler {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
action = MainActivity.SHORTCUT_DOWNLOADS
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
/**
@ -32,13 +30,12 @@ object NotificationHandler {
* @param context context of application
* @param file file containing image
*/
internal fun openImagePendingActivity(context: Context, file: File): PendingIntent {
internal fun openImagePendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "image/*")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
/**
@ -52,6 +49,11 @@ object NotificationHandler {
setDataAndType(uri, ExtensionInstaller.APK_MIME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
return PendingIntent.getActivity(context, 0, intent, 0)
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
}
fun openUrl(context: Context, url: String): PendingIntent {
val notificationIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
return PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
}

View File

@ -59,14 +59,14 @@ class NotificationReceiver : BroadcastReceiver() {
shareImage(
context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
// Delete image from path and dismiss notification
ACTION_DELETE_IMAGE ->
deleteImage(
context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
// Share backup file
ACTION_SHARE_BACKUP ->
@ -74,11 +74,11 @@ class NotificationReceiver : BroadcastReceiver() {
context,
intent.getParcelableExtra(EXTRA_URI)!!,
"application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
ACTION_CANCEL_RESTORE -> cancelRestore(
context,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
@ -87,7 +87,7 @@ class NotificationReceiver : BroadcastReceiver() {
openChapter(
context,
intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
intent.getLongExtra(EXTRA_CHAPTER_ID, -1),
)
}
// Mark updated manga chapters as read
@ -120,7 +120,7 @@ class NotificationReceiver : BroadcastReceiver() {
context,
intent.getParcelableExtra(EXTRA_URI)!!,
"text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
}
}
@ -461,7 +461,7 @@ class NotificationReceiver : BroadcastReceiver() {
context: Context,
manga: Manga,
chapters: Array<Chapter>,
groupId: Int
groupId: Int,
): PendingIntent {
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_MARK_AS_READ
@ -483,7 +483,7 @@ class NotificationReceiver : BroadcastReceiver() {
context: Context,
manga: Manga,
chapters: Array<Chapter>,
groupId: Int
groupId: Int,
): PendingIntent {
val newIntent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_DOWNLOAD_CHAPTER

View File

@ -28,6 +28,8 @@ object Notifications {
const val ID_LIBRARY_PROGRESS = -101
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
const val ID_LIBRARY_SKIPPED = -103
/**
* Notification channel and ids used by the downloader.
@ -114,7 +116,7 @@ object Notifications {
buildNotificationChannelGroup(GROUP_APK_UPDATES) {
setName(context.getString(R.string.label_recent_updates))
},
)
),
)
notificationService.createNotificationChannelsCompat(
@ -132,6 +134,11 @@ object Notifications {
setGroup(GROUP_LIBRARY)
setShowBadge(false)
},
buildNotificationChannel(CHANNEL_LIBRARY_SKIPPED, IMPORTANCE_LOW) {
setName(context.getString(R.string.channel_skipped))
setGroup(GROUP_LIBRARY)
setShowBadge(false)
},
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
setName(context.getString(R.string.channel_new_chapters))
},
@ -175,7 +182,7 @@ object Notifications {
setGroup(GROUP_APK_UPDATES)
setName(context.getString(R.string.channel_ext_updates))
},
)
),
)
}
}

View File

@ -31,6 +31,8 @@ object PreferenceKeys {
const val filterUnread = "pref_filter_library_unread"
const val filterStarted = "pref_filter_library_started"
const val filterCompleted = "pref_filter_library_completed"
const val filterTracked = "pref_filter_library_tracked"

View File

@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.R
const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_CHARGING = "ac"
const val MANGA_ONGOING = "manga_ongoing"
const val MANGA_FULLY_READ = "manga_fully_read"
const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_HAS_UNREAD = "manga_fully_read"
const val MANGA_NON_READ = "manga_started"
/**
* This class stores the values for the preferences in the application.
@ -55,16 +56,22 @@ object PreferenceValues {
LOWEST(47),
}
enum class TabletUiMode {
AUTOMATIC,
ALWAYS,
LANDSCAPE,
NEVER,
enum class TabletUiMode(val titleResId: Int) {
AUTOMATIC(R.string.automatic_background),
ALWAYS(R.string.lock_always),
LANDSCAPE(R.string.landscape),
NEVER(R.string.lock_never),
}
enum class ExtensionInstaller {
LEGACY,
PACKAGEINSTALLER,
SHIZUKU,
enum class ExtensionInstaller(val titleResId: Int) {
LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU(R.string.ext_installer_shizuku),
}
enum class SecureScreenMode(val titleResId: Int) {
ALWAYS(R.string.lock_always),
INCOGNITO(R.string.pref_incognito_mode),
NEVER(R.string.lock_never),
}
}

View File

@ -6,7 +6,7 @@ import android.os.Environment
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.tfcporciuncula.flow.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
@ -34,13 +34,13 @@ class PreferencesHelper(val context: Context) {
private val defaultDownloadsDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"downloads"
"downloads",
).toUri()
private val defaultBackupDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name),
"backup"
"backup",
).toUri()
fun startScreen() = prefs.getInt(Keys.startScreen, 1)
@ -57,7 +57,7 @@ class PreferencesHelper(val context: Context) {
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
fun secureScreen() = flowPrefs.getBoolean("secure_screen", false)
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
@ -67,12 +67,12 @@ class PreferencesHelper(val context: Context) {
fun themeMode() = flowPrefs.getEnum(
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light },
)
fun appTheme() = flowPrefs.getEnum(
"pref_app_theme",
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT }
if (DeviceUtil.isDynamicColorAvailable) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT },
)
fun themeDarkAmoled() = flowPrefs.getBoolean("pref_theme_dark_amoled_key", false)
@ -129,12 +129,14 @@ class PreferencesHelper(val context: Context) {
fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
fun navigateToPan() = flowPrefs.getBoolean("navigate_pan", true)
fun landscapeZoom() = flowPrefs.getBoolean("landscape_zoom", true)
fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)
fun readWithTapping() = flowPrefs.getBoolean("reader_tap", true)
fun pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", Values.TappingInvertMode.NONE)
@ -220,7 +222,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
fun libraryUpdateDeviceRestriction() = flowPrefs.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_FULLY_READ, MANGA_ONGOING))
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_HAS_UNREAD, MANGA_NON_COMPLETED, MANGA_NON_READ))
fun showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0)
@ -248,6 +250,8 @@ class PreferencesHelper(val context: Context) {
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterStarted() = flowPrefs.getInt(Keys.filterStarted, 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)
@ -312,7 +316,7 @@ class PreferencesHelper(val context: Context) {
fun extensionInstaller() = flowPrefs.getEnum(
"extension_installer",
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
)
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)

View File

@ -0,0 +1,148 @@
package eu.kanade.tachiyomi.data.saver
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.IOException
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
class ImageSaver(
val context: Context,
) {
@SuppressLint("InlinedApi")
fun save(image: Image): Uri {
val data = image.data
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return save(data(), image.location.directory(context), filename)
}
if (image.location !is Location.Pictures) {
return save(data(), image.location.directory(context), filename)
}
val pictureDir =
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, image.name)
put(MediaStore.Images.Media.MIME_TYPE, type.mime)
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/${context.getString(R.string.app_name)}/" +
(image.location as Location.Pictures).relativePath,
)
}
val picture = context.contentResolver.insert(
pictureDir,
contentValues,
) ?: throw IOException("Couldn't create file")
data().use { input ->
@Suppress("BlockingMethodInNonBlockingContext")
context.contentResolver.openOutputStream(picture, "w").use { output ->
input.copyTo(output!!)
}
}
DiskUtil.scanMedia(context, picture)
return picture
}
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
directory.mkdirs()
val destFile = File(directory, filename)
inputStream.use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
DiskUtil.scanMedia(context, destFile)
return destFile.getUriCompat(context)
}
}
sealed class Image(
open val name: String,
open val location: Location,
) {
data class Cover(
val bitmap: Bitmap,
override val name: String,
override val location: Location,
) : Image(name, location)
data class Page(
val inputStream: () -> InputStream,
override val name: String,
override val location: Location,
) : Image(name, location)
val data: () -> InputStream
get() {
return when (this) {
is Cover -> {
{
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
ByteArrayInputStream(baos.toByteArray())
}
}
is Page -> inputStream
}
}
}
sealed class Location {
data class Pictures private constructor(val relativePath: String) : Location() {
companion object {
fun create(relativePath: String = ""): Pictures {
return Pictures(relativePath)
}
}
}
object Cache : Location()
fun directory(context: Context): File {
return when (this) {
Cache -> context.cacheImageDir
is Pictures -> {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
context.getString(R.string.app_name),
)
if (relativePath.isNotEmpty()) {
return File(
file,
relativePath,
)
}
file
}
}
}
}

View File

@ -43,7 +43,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
| status
|}
|}
|""".trimMargin()
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
@ -55,8 +56,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
body = payload.toString().toRequestBody(jsonMime),
),
)
.await()
.parseAs<JsonObject>()
@ -84,7 +85,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|progress
|}
|}
|""".trimMargin()
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
@ -127,7 +129,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|}
|""".trimMargin()
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
@ -137,8 +140,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
body = payload.toString().toRequestBody(jsonMime),
),
)
.await()
.parseAs<JsonObject>()
@ -193,7 +196,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|}
|""".trimMargin()
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
@ -204,8 +208,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
body = payload.toString().toRequestBody(jsonMime),
),
)
.await()
.parseAs<JsonObject>()
@ -238,15 +242,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|}
|""".trimMargin()
|
""".trimMargin()
val payload = buildJsonObject {
put("query", query)
}
authClient.newCall(
POST(
apiUrl,
body = payload.toString().toRequestBody(jsonMime)
)
body = payload.toString().toRequestBody(jsonMime),
),
)
.await()
.parseAs<JsonObject>()
@ -255,7 +260,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val viewer = data["Viewer"]!!.jsonObject
Pair(
viewer["id"]!!.jsonPrimitive.int,
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
)
}
}
@ -270,7 +275,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0,
)
}
@ -282,7 +287,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject)
jsonToALManga(struct["media"]!!.jsonObject),
)
}
@ -292,7 +297,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
)
date.timeInMillis
} catch (_: Exception) {

View File

@ -16,7 +16,7 @@ data class ALManga(
val format: String,
val publishing_status: String,
val start_date_fuzzy: Long,
val total_chapters: Int
val total_chapters: Int,
) {
fun toTrack() = TrackSearch.create(TrackManager.ANILIST).apply {
@ -46,7 +46,7 @@ data class ALUserManga(
val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga
val manga: ALManga,
) {
fun toTrack() = Track.create(TrackManager.ANILIST).apply {

View File

@ -7,7 +7,7 @@ data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long
val expires_in: Long,
) {
fun isExpired() = System.currentTimeMillis() > expires

View File

@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",
val small: String? = ""
val small: String? = "",
)

View File

@ -61,8 +61,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
authClient.newCall(
POST(
"$apiUrl/subject/${track.media_id}/update/watched_eps",
body = body
)
body = body,
),
).await()
track
@ -168,7 +168,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("client_secret", clientSecret)
.add("code", code)
.add("redirect_uri", redirectUrl)
.build()
.build(),
)
companion object {
@ -201,7 +201,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("client_secret", clientSecret)
.add("refresh_token", token)
.add("redirect_uri", redirectUrl)
.build()
.build(),
)
}
}

View File

@ -34,7 +34,7 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
.header("User-Agent", "Tachiyomi")
.url(
originalRequest.url.newBuilder()
.addQueryParameter("access_token", currAuth.access_token).build()
.addQueryParameter("access_token", currAuth.access_token).build(),
)
.build() else originalRequest.newBuilder()
.post(addToken(currAuth.access_token, originalRequest.body as FormBody))
@ -51,7 +51,7 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
System.currentTimeMillis() / 1000,
oauth.expires_in,
oauth.refresh_token,
this.oauth?.user_id
this.oauth?.user_id,
)
bangumi.saveToken(oauth)

View File

@ -12,5 +12,5 @@ data class Collection(
val status: Status? = Status(),
val tag: List<String?>? = listOf(),
val user: User? = User(),
val vol_status: Int? = 0
val vol_status: Int? = 0,
)

View File

@ -9,7 +9,7 @@ data class OAuth(
val created_at: Long = System.currentTimeMillis() / 1000,
val expires_in: Long,
val refresh_token: String?,
val user_id: Long?
val user_id: Long?,
) {
// Access token refresh before expired

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