Compare commits

...

131 Commits

Author SHA1 Message Date
28522f4f90 Release v0.13.3 2022-04-15 16:37:25 -04:00
ec3a227a02 Weblate translations (#6890)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@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: GTX155 <kirchoabv@mail.bg>
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: 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: Lauri <lauri.kangasaho@hotmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: 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: Pilfer <pescao@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rikishaaa <jebote90@gmail.com>
Co-authored-by: Santiago José Gutiérrez Llanod <gutierrezapata17@gmail.com>
Co-authored-by: Sebastian Mihai Crap <sebastiancrap@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Zero O <godarms2010@live.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/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/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/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/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/or/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/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: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@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: GTX155 <kirchoabv@mail.bg>
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: Jetspectre <jetspectre1@gmail.com>
Co-authored-by: Lauri <lauri.kangasaho@hotmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: 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: Pilfer <pescao@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rikishaaa <jebote90@gmail.com>
Co-authored-by: Santiago José Gutiérrez Llanod <gutierrezapata17@gmail.com>
Co-authored-by: Sebastian Mihai Crap <sebastiancrap@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Swyter <swyterzone@gmail.com>
Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
Co-authored-by: Zero O <godarms2010@live.com>
Co-authored-by: altinat <poiiiii4yy@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: אילון קטן <eilonkatan@gmail.com>
2022-04-15 16:35:30 -04:00
89decf3474 Always remove manga title from if it prefixes chapter names (related to #6913) 2022-04-15 15:52:48 -04:00
0b2794e843 Limit package name overriding to Android 8+ (related to #6846) 2022-04-15 13:43:54 -04:00
554dfb5874 Bump Material Components 2022-04-15 13:36:24 -04:00
9c30fa1da3 Update F-Droid migration guide link 2022-04-15 12:11:01 -04:00
e81bd61e24 Adjust update/download warnings
This is a partial revert/evolution of 538dd60580

- Back to notifications, because Android 12+ may cut off toasts
- Notifications now automatically dismiss after 30s on Android 8+ (taken from J2K)
- Also warn if more than 30 chapters are queued for download
2022-04-15 10:24:54 -04:00
7a0b54bb38 Set network call timeout to 90 seconds (instead of infinite) 2022-04-15 09:56:35 -04:00
f060daf8c4 Rollback to stable OkHttp
There's some weird crashes related to it. Happy Eyeballs will return once we upgrade again.
2022-04-14 22:37:51 -04:00
c1976ef599 Avoid some crashes 2022-04-14 18:28:16 -04:00
f16fb4e1e4 Minor cleanup 2022-04-14 18:15:47 -04:00
5da2c82f47 Avoid crashing if picture can't be saved (related to #6905) 2022-04-13 18:45:49 -04:00
d443245d66 Update Skip Updating preference strings. (#6900)
* Update Skip Updating preference strings.

* Complete -> Completed

* hasn't -> haven't

* Apply suggestions from code review

Co-authored-by: arkon <arkon@users.noreply.github.com>

Co-authored-by: arkon <arkon@users.noreply.github.com>
2022-04-13 18:35:58 -04:00
9be3eea5fd Remove dependency review step from push workflow 2022-04-13 18:35:46 -04:00
07a9fd061d Add dependency review step to workflows 2022-04-13 18:34:33 -04:00
2a070c0b1e Add clear cookies option to WebView menu 2022-04-13 17:48:05 -04:00
7b5106d206 Update ACRA 2022-04-13 17:44:49 -04:00
821d9cdb02 Show different update notification for F-Droid installations 2022-04-13 17:44:43 -04:00
28575936d3 Move learn more text in skipped entries notification to main content
Because people apparently don't realize they can tap actions
2022-04-12 23:08:00 -04:00
83a04da4a0 Stop allowing keeping app data on uninstall
Seems to be more trouble than it's worth since it makes the app uninstallable without manually deleting app data. Users have to go out of their way to save data into the app data folder now anyway.
2022-04-12 17:27:09 -04:00
0894b1394f Fix cover sharing error string (#6911) 2022-04-12 09:10:32 -04:00
eb33d3c991 Remove build flavor checks for update warnings
"stable" was invalid anyway, it should've been "release"
2022-04-11 23:05:00 -04:00
d7f01abf3a Update Coil 2022-04-11 23:04:19 -04:00
80635343ae Update ACRA 2022-04-11 23:04:07 -04:00
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
447 changed files with 6618 additions and 3882 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 acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v0.13.1) - To the latest version of the app (stable is v0.13.3)
- All extensions - All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - 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 - 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 label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "0.13.1" Example: "0.13.3"
validations: validations:
required: true required: true
@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[0.13.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.13.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true

View File

@ -33,7 +33,7 @@ body:
required: true 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). - 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 required: true
- label: I have updated the app to version **[0.13.1](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. - label: I have updated the app to version **[0.13.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View File

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

View File

@ -5,6 +5,9 @@ on:
- '**.md' - '**.md'
- 'app/src/main/res/**/strings.xml' - 'app/src/main/res/**/strings.xml'
permissions:
contents: read
jobs: jobs:
build: build:
name: Build app name: Build app
@ -12,11 +15,14 @@ jobs:
steps: steps:
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
- name: Dependency Review
uses: actions/dependency-review-action@v1
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:

View File

@ -19,7 +19,7 @@ jobs:
all_but_latest: true all_but_latest: true
- name: Clone repo - name: Clone repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1

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

View File

@ -26,7 +26,6 @@
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
@ -181,10 +180,6 @@
android:name=".data.updater.AppUpdateService" android:name=".data.updater.AppUpdateService"
android:exported="false" /> android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false" />
<service <service
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false" /> android:exported="false" />

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import android.annotation.SuppressLint
import android.app.ActivityManager import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.app.PendingIntent import android.app.PendingIntent
@ -8,6 +9,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.Looper
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -20,17 +22,19 @@ import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.decode.GifDecoder import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger import coil.util.DebugLogger
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher 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.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.NetworkHelper 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.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil 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.animatorDurationScale
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
@ -54,6 +58,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
private val disableIncognitoReceiver = DisableIncognitoReceiver() private val disableIncognitoReceiver = DisableIncognitoReceiver()
@SuppressLint("LaunchActivityFromNotification")
override fun onCreate() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
@ -91,7 +96,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
this@App, this@App,
0, 0,
Intent(ACTION_DISABLE_INCOGNITO_MODE), Intent(ACTION_DISABLE_INCOGNITO_MODE),
PendingIntent.FLAG_ONE_SHOT PendingIntent.FLAG_ONE_SHOT,
) )
setContentIntent(pendingIntent) setContentIntent(pendingIntent)
} }
@ -110,7 +115,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} },
) )
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope) }.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
@ -121,17 +126,20 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this).apply { 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder(this@App)) add(ImageDecoderDecoder.Factory())
} else { } else {
add(GifDecoder()) add(GifDecoder.Factory())
} }
add(TachiyomiImageDecoder(this@App.resources)) add(TachiyomiImageDecoder.Factory())
add(ByteBufferFetcher()) add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverFetcher()) add(MangaCoverKeyer())
} }
okHttpClient(Injekt.get<NetworkHelper>().coilClient) callFactory(callFactoryInit)
diskCache(diskCacheInit)
crossfade((300 * this@App.animatorDurationScale).toInt()) crossfade((300 * this@App.animatorDurationScale).toInt())
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice) allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
if (preferences.verboseLogging()) logger(DebugLogger()) if (preferences.verboseLogging()) logger(DebugLogger())
@ -144,11 +152,32 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
} }
override fun getPackageName(): String {
// This causes freezes in Android 6/7 for some reason
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val chromiumElement = stackTrace.find {
it.className.equals(
"org.chromium.base.BuildInfo",
ignoreCase = true,
)
}
if (chromiumElement?.methodName.equals("getAll", ignoreCase = true)) {
return WebViewUtil.SPOOF_PACKAGE_NAME
}
} catch (e: Exception) {
}
}
return super.getPackageName()
}
protected open fun setupAcra() { protected open fun setupAcra() {
if (BuildConfig.FLAVOR != "dev") { if (BuildConfig.FLAVOR != "dev") {
initAcra { initAcra {
buildConfigClass = BuildConfig::class.java buildConfigClass = BuildConfig::class.java
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*") excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")
httpSender { httpSender {
uri = BuildConfig.ACRA_URI uri = BuildConfig.ACRA_URI
@ -190,3 +219,24 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE" 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.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.TrackManager
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
@ -46,6 +47,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DelayedTrackingStore(app) } addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute { ContextCompat.getMainExecutor(app).execute {
get<PreferencesHelper>() get<PreferencesHelper>()

View File

@ -5,8 +5,9 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob 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.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.AppUpdateJob 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.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.util.preference.minusAssign import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign 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.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -242,7 +244,26 @@ object Migrations {
if (oldVersion < 72) { if (oldVersion < 72) {
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true) val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) { 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() internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper 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 * Returns manga

View File

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

View File

@ -11,4 +11,15 @@ object BackupConst {
const val BACKUP_TYPE_LEGACY = 0 const val BACKUP_TYPE_LEGACY = 0
const val BACKUP_TYPE_FULL = 1 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 package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters 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.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.notificationManager
import logcat.LogPriority import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -20,37 +28,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val uri = preferences.backupsDirectory().get().toUri() val notifier = BackupNotifier(context)
val flags = BackupCreateService.BACKUP_ALL 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 { 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() Result.success()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
if (!isAutoBackup) notifier.showBackupError(e.message)
Result.failure() Result.failure()
} finally {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
} }
} }
companion object { 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) { fun setupTask(context: Context, prefInterval: Int? = null) {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().get() val interval = prefInterval ?: preferences.backupInterval().get()
val workManager = WorkManager.getInstance(context)
if (interval > 0) { if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), interval.toLong(),
TimeUnit.HOURS, TimeUnit.HOURS,
10, 10,
TimeUnit.MINUTES TimeUnit.MINUTES,
) )
.addTag(TAG) .addTag(TAG_AUTO)
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build() .build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
} else { } 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( addAction(
R.drawable.ic_share_24dp, R.drawable.ic_share_24dp,
context.getString(R.string.action_share), 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) show(Notifications.ID_BACKUP_COMPLETE)
@ -97,7 +97,7 @@ class BackupNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_stop), 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, R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time), TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time) TimeUnit.MILLISECONDS.toMinutes(time),
) ),
) )
with(completeNotificationBuilder) { with(completeNotificationBuilder) {

View File

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

View File

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

View File

@ -9,5 +9,5 @@ data class Backup(
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(), @ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value // Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(), @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( return BackupCategory(
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags flags = category.flags,
) )
} }
} }

View File

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

View File

@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class BrokenBackupHistory( data class BrokenBackupHistory(
@ProtoNumber(0) var url: String, @ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long @ProtoNumber(1) var lastRead: Long,
) )
@Serializable @Serializable
data class BackupHistory( data class BackupHistory(
@ProtoNumber(1) var url: String, @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(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(), @ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null, @ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList() @ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
) { ) {
fun getMangaImpl(): MangaImpl { fun getMangaImpl(): MangaImpl {
return MangaImpl().apply { return MangaImpl().apply {
@ -83,7 +83,7 @@ data class BackupManga(
dateAdded = manga.date_added, dateAdded = manga.date_added,
viewer = manga.readingModeType, viewer = manga.readingModeType,
viewer_flags = manga.viewer_flags, 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 @Serializable
data class BrokenBackupSource( data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "", @ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long @ProtoNumber(1) var sourceId: Long,
) )
@Serializable @Serializable
data class BackupSource( data class BackupSource(
@ProtoNumber(1) var name: String = "", @ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long @ProtoNumber(2) var sourceId: Long,
) { ) {
companion object { companion object {
fun copyFrom(source: Source): BackupSource { fun copyFrom(source: Source): BackupSource {
return BackupSource( return BackupSource(
name = source.name, name = source.name,
sourceId = source.id sourceId = source.id,
) )
} }
} }

View File

@ -56,7 +56,7 @@ data class BackupTracking(
status = track.status, status = track.status,
startedReadingDate = track.started_reading_date, startedReadingDate = track.started_reading_date,
finishedReadingDate = track.finished_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 * Create backup Json file from database
* *
* @param uri path of Uri * @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") throw IllegalStateException("Legacy backup creation is not supported")
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { 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, // Read the json and create a Json Object,
// cannot use the backupManager json deserializer one because its not initialized yet // cannot use the backupManager json deserializer one because its not initialized yet
val backupObject = Json.decodeFromStream<JsonObject>( val backupObject = Json.decodeFromStream<JsonObject>(
context.contentResolver.openInputStream(uri)!! context.contentResolver.openInputStream(uri)!!,
) )
// Get parser version // Get parser version
@ -109,7 +109,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>,
) { ) {
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
@ -139,7 +139,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>,
) { ) {
try { try {
val fetchedManga = backupManager.fetchManga(source, manga) val fetchedManga = backupManager.fetchManga(source, manga)
@ -161,7 +161,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>,
) { ) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) { if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters) updateChapters(source, backupManga, chapters)

View File

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

View File

@ -15,7 +15,7 @@ data class Backup(
val version: Int? = null, val version: Int? = null,
var mangas: MutableList<MangaObject> = mutableListOf(), var mangas: MutableList<MangaObject> = mutableListOf(),
var categories: List<@Contextual Category>? = null, var categories: List<@Contextual Category>? = null,
var extensions: List<String>? = null var extensions: List<String>? = null,
) { ) {
companion object { companion object {
const val CURRENT_VERSION = 2 const val CURRENT_VERSION = 2
@ -33,5 +33,5 @@ data class MangaObject(
var chapters: List<@Contextual Chapter>? = null, var chapters: List<@Contextual Chapter>? = null,
var categories: List<String>? = null, var categories: List<String>? = null,
var track: List<@Contextual Track>? = 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 { buildJsonArray {
add(value.name) add(value.name)
add(value.order) add(value.order)
} },
) )
} }

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class ChapterCache(private val context: Context) {
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY), File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION, PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT, 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. * Clear coil's memory cache.
*/ */
fun clearMemoryCache() { fun clearMemoryCache() {
context.imageLoader.memoryCache.clear() context.imageLoader.memoryCache?.clear()
} }
private fun getCacheDir(dir: String): File { 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 package eu.kanade.tachiyomi.data.coil
import coil.bitmap.BitmapPool import coil.ImageLoader
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.Options import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.network.HttpException import coil.network.HttpException
import coil.request.get import coil.request.Options
import coil.size.Size import coil.request.Parameters
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.internal.closeQuietly
import okio.Path.Companion.toOkioPath
import okio.Source
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File 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: * Available request parameter:
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
*/ */
class MangaCoverFetcher : Fetcher<Manga> { class MangaCoverFetcher(
private val coverCache: CoverCache by injectLazy() private val manga: Manga,
private val sourceManager: SourceManager by injectLazy() private val sourceLazy: Lazy<HttpSource?>,
private val defaultClient = Injekt.get<NetworkHelper>().coilClient 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? { // For non-custom cover
if (data.thumbnail_url.isNullOrBlank()) return null private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
return data.thumbnail_url!! 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 // Use custom cover if exists
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true
val customCoverFile = coverCache.getCustomCoverFile(data) val customCoverFile = coverCache.getCustomCoverFile(manga)
if (useCustomCover && customCoverFile.exists()) { if (useCustomCover && customCoverFile.exists()) {
return fileLoader(customCoverFile) return fileLoader(customCoverFile)
} }
val cover = data.thumbnail_url // diskCacheKey is thumbnail_url
return when (getResourceType(cover)) { url = diskCacheKey ?: error("No cover specified")
Type.URL -> httpLoader(data, options) return when (getResourceType(url)) {
Type.File -> fileLoader(data) Type.URL -> httpLoader()
Type.File -> fileLoader(File(url.substringAfter("file://")))
null -> error("Invalid image") 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 // 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") coverCache.getCoverFile(manga) ?: error("No cover specified")
} else { } else {
null null
} }
if (libraryCoverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) { return fileLoader(libraryCoverCacheFile)
return fileLoader(coverCacheFile)
} }
val (response, body) = awaitGetCall(manga, options) var snapshot = readFromDiskCache()
if (!response.isSuccessful) { try {
body.close() // 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) throw HttpException(response)
} }
return response
}
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) { private fun newRequest(): Request {
@Suppress("BlockingMethodInNonBlockingContext") val request = Request.Builder()
response.peekBody(Long.MAX_VALUE).source().use { input -> .url(url)
coverCacheFile.parentFile?.mkdirs() .headers(sourceLazy.value?.headers ?: options.headers)
if (coverCacheFile.exists()) { // Support attaching custom data to the network request.
coverCacheFile.delete() .tag(Parameters::class.java, options.parameters)
}
coverCacheFile.sink().buffer().use { output -> val diskRead = options.diskCachePolicy.readEnabled
output.writeAll(input) 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( return request.build()
source = body.source(),
mimeType = "image/*",
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
)
} }
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> { private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
val call = getCall(manga, options) if (cacheFile == null) return null
val response = call.await() return try {
return response to checkNotNull(response.body) { "Null response source" } diskCacheLazy.value.run {
} fileSystem.source(snapshot.data).use { input ->
writeSourceToCoverCache(input, cacheFile)
private fun getCall(manga: Manga, options: Options): Call { }
val source = sourceManager.get(manga.source) as? HttpSource remove(diskCacheKey!!)
val request = Request.Builder().url(manga.thumbnail_url!!).also {
if (source != null) {
it.headers(source.headers)
} }
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 private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
val diskRead = options.diskCachePolicy.readEnabled if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
when { return try {
!networkRead && diskRead -> { response.peekBody(Long.MAX_VALUE).source().use { input ->
it.cacheControl(CacheControl.FORCE_CACHE) writeSourceToCoverCache(input, cacheFile)
}
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)
}
} }
}.build() cacheFile.takeIf { it.exists() }
} catch (e: Exception) {
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient logcat(LogPriority.ERROR, e) { "Failed to write response data to cover cache ${cacheFile.name}" }
return client.newCall(request) null
}
} }
private fun fileLoader(manga: Manga): FetchResult { private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
return fileLoader(File(manga.thumbnail_url!!.substringAfter("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 { private fun readFromDiskCache(): DiskCache.Snapshot? {
return SourceResult( return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
source = file.source().buffer(), }
mimeType = "image/*",
dataSource = DataSource.DISK 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? { private fun getResourceType(cover: String?): Type? {
@ -159,6 +271,20 @@ class MangaCoverFetcher : Fetcher<Manga> {
File, URL 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 { companion object {
const val USE_CUSTOM_COVER = "use_custom_cover" 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 package eu.kanade.tachiyomi.data.coil
import android.content.res.Resources
import android.os.Build import android.os.Build
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
import coil.bitmap.BitmapPool import coil.ImageLoader
import coil.decode.DecodeResult import coil.decode.DecodeResult
import coil.decode.Decoder import coil.decode.Decoder
import coil.decode.Options import coil.decode.ImageDecoderDecoder
import coil.size.Size import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource import okio.BufferedSource
import tachiyomi.decoder.ImageDecoder 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. * 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 { override suspend fun decode(): DecodeResult {
val type = source.peek().inputStream().use { val decoder = resources.sourceOrNull()?.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 {
ImageDecoder.newInstance(it.inputStream()) ImageDecoder.newInstance(it.inputStream())
} }
@ -46,8 +31,31 @@ class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
check(bitmap != null) { "Failed to decode image." } check(bitmap != null) { "Failed to decode image." }
return DecodeResult( return DecodeResult(
drawable = bitmap.toDrawable(resources), drawable = bitmap.toDrawable(options.context.resources),
isSampled = false 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 // Fix kissmanga covers after supporting cloudflare
db.execSQL( db.execSQL(
"""UPDATE mangas SET thumbnail_url = """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) { if (oldVersion < 3) {

View File

@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>( class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(), CategoryPutResolver(),
CategoryGetResolver(), CategoryGetResolver(),
CategoryDeleteResolver() CategoryDeleteResolver(),
) )
class CategoryPutResolver : DefaultPutResolver<Category>() { class CategoryPutResolver : DefaultPutResolver<Category>() {
@ -40,7 +40,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
COL_ID to obj.id, COL_ID to obj.id,
COL_NAME to obj.name, COL_NAME to obj.name,
COL_ORDER to obj.order, 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>( class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(), ChapterPutResolver(),
ChapterGetResolver(), ChapterGetResolver(),
ChapterDeleteResolver() ChapterDeleteResolver(),
) )
class ChapterPutResolver : DefaultPutResolver<Chapter>() { class ChapterPutResolver : DefaultPutResolver<Chapter>() {
@ -56,7 +56,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
COL_DATE_UPLOAD to obj.date_upload, COL_DATE_UPLOAD to obj.date_upload,
COL_LAST_PAGE_READ to obj.last_page_read, COL_LAST_PAGE_READ to obj.last_page_read,
COL_CHAPTER_NUMBER to obj.chapter_number, 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>( class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(), HistoryPutResolver(),
HistoryGetResolver(), HistoryGetResolver(),
HistoryDeleteResolver() HistoryDeleteResolver(),
) )
open class HistoryPutResolver : DefaultPutResolver<History>() { open class HistoryPutResolver : DefaultPutResolver<History>() {
@ -40,7 +40,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
COL_ID to obj.id, COL_ID to obj.id,
COL_CHAPTER_ID to obj.chapter_id, COL_CHAPTER_ID to obj.chapter_id,
COL_LAST_READ to obj.last_read, 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>( class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(), MangaCategoryPutResolver(),
MangaCategoryGetResolver(), MangaCategoryGetResolver(),
MangaCategoryDeleteResolver() MangaCategoryDeleteResolver(),
) )
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
@ -37,7 +37,7 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
contentValuesOf( contentValuesOf(
COL_ID to obj.id, COL_ID to obj.id,
COL_MANGA_ID to obj.manga_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>( class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(), MangaPutResolver(),
MangaGetResolver(), MangaGetResolver(),
MangaDeleteResolver() MangaDeleteResolver(),
) )
class MangaPutResolver : DefaultPutResolver<Manga>() { class MangaPutResolver : DefaultPutResolver<Manga>() {
@ -66,7 +66,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_VIEWER to obj.viewer_flags, COL_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags, COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified, 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>( class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(), TrackPutResolver(),
TrackGetResolver(), TrackGetResolver(),
TrackDeleteResolver() TrackDeleteResolver(),
) )
class TrackPutResolver : DefaultPutResolver<Track>() { class TrackPutResolver : DefaultPutResolver<Track>() {
@ -58,7 +58,7 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
COL_TRACKING_URL to obj.tracking_url, COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score, COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date, 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() { 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 var category: Int = 0
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,11 +29,26 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(libraryQuery) .query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build() .build(),
) )
.withGetResolver(LibraryMangaGetResolver.INSTANCE) .withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare() .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> { fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
var queryBuilder = Query.builder() var queryBuilder = Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
@ -57,7 +72,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId) .whereArgs(url, sourceId)
.build() .build(),
) )
.prepare() .prepare()
@ -68,7 +83,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(id) .whereArgs(id)
.build() .build(),
) )
.prepare() .prepare()
@ -78,7 +93,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getSourceIdsWithNonLibraryMangaQuery()) .query(getSourceIdsWithNonLibraryMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE) .withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
.prepare() .prepare()
@ -137,7 +152,7 @@ interface MangaQueries : DbProvider {
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})") .where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
.whereArgs(0, *sourceIds.toTypedArray()) .whereArgs(0, *sourceIds.toTypedArray())
.build() .build(),
) )
.prepare() .prepare()
@ -145,7 +160,7 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@ -155,7 +170,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLastReadMangaQuery()) .query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@ -165,7 +180,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getTotalChapterMangaQuery()) .query(getTotalChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@ -175,7 +190,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLatestChapterMangaQuery()) .query(getLatestChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@ -185,7 +200,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getChapterFetchDateMangaQuery()) .query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .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 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 = val libraryQuery =
""" """
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} SELECT ${Manga.TABLE}.*, COALESCE(C.unreadCount, 0) AS ${Manga.COMPUTED_COL_UNREAD_COUNT}, COALESCE(R.readCount, 0) AS ${Manga.COMPUTED_COL_READ_COUNT}
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unreadCount
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0 WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.COL_MANGA_ID}
) AS C ) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} 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 WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID} GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE} ORDER BY ${Manga.COL_TITLE}

View File

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

View File

@ -29,6 +29,6 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
contentValuesOf( contentValuesOf(
ChapterTable.COL_READ to chapter.read, ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark, 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( contentValuesOf(
ChapterTable.COL_READ to chapter.read, ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark, 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( contentValuesOf(
ChapterTable.COL_READ to chapter.read, ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark, 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) = fun mapToContentValues(chapter: Chapter) =
contentValuesOf( 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()) .table(updateQuery.table())
.where(updateQuery.where()) .where(updateQuery.where())
.whereArgs(updateQuery.whereArgs()) .whereArgs(updateQuery.whereArgs())
.build() .build(),
) )
cursor.use { putCursor -> cursor.use { putCursor ->
@ -47,6 +47,6 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
private fun mapToUpdateContentValues(history: History) = private fun mapToUpdateContentValues(history: History) =
contentValuesOf( 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() val manga = LibraryManga()
mapBaseFromCursor(manga, cursor) 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.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
return manga return manga
} }

View File

@ -27,6 +27,6 @@ class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
fun mapToContentValues(manga: Manga) = fun mapToContentValues(manga: Manga) =
contentValuesOf( 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) = fun mapToContentValues(manga: Manga) =
contentValuesOf( 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) = fun mapToContentValues(manga: Manga) =
contentValuesOf( 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) = fun mapToContentValues(manga: Manga) =
contentValuesOf( 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) = fun mapToContentValues(manga: Manga) =
contentValuesOf( 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_CHAPTER_FLAGS = "chapter_flags"
const val COL_UNREAD = "unread"
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
const val COL_COVER_LAST_MODIFIED = "cover_last_modified" const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
// 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 val createTableQuery: String
get() = get() =
"""CREATE TABLE $TABLE( """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) |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 |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 |FROM ${TABLE}_tmp
""".trimMargin() """.trimMargin()
val dropTempTable: String val dropTempTable: String
get() = "DROP TABLE ${TABLE}_tmp" get() = "DROP TABLE ${TABLE}_tmp"

View File

@ -27,7 +27,7 @@ class DownloadCache(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val sourceManager: SourceManager, 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( private class RootDirectory(
val dir: UniFile, val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf() var files: Map<Long, SourceDirectory> = hashMapOf(),
) )
/** /**
@ -244,7 +244,7 @@ class DownloadCache(
*/ */
private class SourceDirectory( private class SourceDirectory(
val dir: UniFile, val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf() var files: Map<String, MangaDirectory> = hashMapOf(),
) )
/** /**
@ -252,7 +252,7 @@ class DownloadCache(
*/ */
private class MangaDirectory( private class MangaDirectory(
val dir: UniFile, 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( class DownloadManager(
private val context: Context, private val context: Context,
private val db: DatabaseHelper = Injekt.get() private val db: DatabaseHelper = Injekt.get(),
) { ) {
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()

View File

@ -93,14 +93,14 @@ internal class DownloadNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_pause_24dp, R.drawable.ic_pause_24dp,
context.getString(R.string.action_pause), context.getString(R.string.action_pause),
NotificationReceiver.pauseDownloadsPendingBroadcast(context) NotificationReceiver.pauseDownloadsPendingBroadcast(context),
) )
} }
val downloadingProgressText = context.getString( val downloadingProgressText = context.getString(
R.string.chapter_downloading_progress, R.string.chapter_downloading_progress,
download.downloadedImages, download.downloadedImages,
download.pages!!.size download.pages!!.size,
) )
if (preferences.hideNotificationContent()) { if (preferences.hideNotificationContent()) {
@ -138,13 +138,13 @@ internal class DownloadNotifier(private val context: Context) {
addAction( addAction(
R.drawable.ic_play_arrow_24dp, R.drawable.ic_play_arrow_24dp,
context.getString(R.string.action_resume), context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context) NotificationReceiver.resumeDownloadsPendingBroadcast(context),
) )
// Clear action // Clear action
addAction( addAction(
R.drawable.ic_close_24dp, R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel_all), context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context) NotificationReceiver.clearDownloadsPendingBroadcast(context),
) )
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS) show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
@ -184,16 +184,19 @@ internal class DownloadNotifier(private val context: Context) {
* Called when the downloader receives a warning. * Called when the downloader receives a warning.
* *
* @param reason the text to show. * @param reason the text to show.
* @param timeout duration after which to automatically dismiss the notification.
* Only works on Android 8+.
*/ */
fun onWarning(reason: String) { fun onWarning(reason: String, timeout: Long? = null) {
with(errorNotificationBuilder) { with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setStyle(NotificationCompat.BigTextStyle().bigText(reason)) setContentText(reason)
setSmallIcon(R.drawable.ic_warning_white_24dp) setSmallIcon(R.drawable.ic_warning_white_24dp)
setAutoCancel(true) setAutoCancel(true)
clearActions() clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
timeout?.let { setTimeoutAfter(it) }
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
} }
@ -213,7 +216,7 @@ internal class DownloadNotifier(private val context: Context) {
// Create notification // Create notification
with(errorNotificationBuilder) { with(errorNotificationBuilder) {
setContentTitle( 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)) setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(R.drawable.ic_warning_white_24dp) setSmallIcon(R.drawable.ic_warning_white_24dp)

View File

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

View File

@ -138,7 +138,7 @@ class DownloadProvider(private val context: Context) {
when { when {
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}" chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
else -> chapter.name else -> chapter.name
} },
) )
} }
@ -157,7 +157,7 @@ class DownloadProvider(private val context: Context) {
"$chapterName.cbz", "$chapterName.cbz",
// Legacy chapter directory name used in v0.9.2 and before // 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() { private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay subscriptions += downloadManager.runningRelay
.doOnError { /* Swallow wakelock error */ } .doOnError {
/* Swallow wakelock error */
}
.subscribe { running -> .subscribe { running ->
if (running) { if (running) {
wakeLock.acquireIfNeeded() wakeLock.acquireIfNeeded()

View File

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

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.util.lang.RetryWithDelay
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.plusAssign 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.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
@ -57,7 +58,7 @@ class Downloader(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val cache: DownloadCache, private val cache: DownloadCache,
private val sourceManager: SourceManager private val sourceManager: SourceManager,
) { ) {
private val chapterCache: ChapterCache by injectLazy() private val chapterCache: ChapterCache by injectLazy()
@ -208,7 +209,7 @@ class Downloader(
downloadChapter(download).subscribeOn(Schedulers.io()) downloadChapter(download).subscribeOn(Schedulers.io())
} }
}, },
5 5,
) )
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -220,7 +221,7 @@ class Downloader(
DownloadService.stop(context) DownloadService.stop(context)
logcat(LogPriority.ERROR, error) logcat(LogPriority.ERROR, error)
notifier.onError(error.message) notifier.onError(error.message)
} },
) )
} }
@ -271,15 +272,21 @@ class Downloader(
// Start downloader if needed // Start downloader if needed
if (autoStart && wasEmpty) { if (autoStart && wasEmpty) {
val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count()
val maxDownloadsFromSource = queue val maxDownloadsFromSource = queue
.groupBy { it.source } .groupBy { it.source }
.filterKeys { it !is UnmeteredSource } .filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size } .maxOf { it.value.size }
// TODO: re-enable warning if (
if (maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
// withUIContext { maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
// context.toast(R.string.download_queue_size_warning, Toast.LENGTH_LONG) ) {
// } withUIContext {
notifier.onWarning(
context.getString(R.string.download_queue_size_warning),
WARNING_NOTIF_TIMEOUT_MS,
)
}
} }
DownloadService.start(context) DownloadService.start(context)
} }
@ -476,7 +483,7 @@ class Downloader(
download: Download, download: Download,
mangaDir: UniFile, mangaDir: UniFile,
tmpDir: UniFile, tmpDir: UniFile,
dirname: String dirname: String,
) { ) {
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -557,9 +564,11 @@ class Downloader(
companion object { companion object {
const val TMP_DIR_SUFFIX = "_tmp" const val TMP_DIR_SUFFIX = "_tmp"
const val WARNING_NOTIF_TIMEOUT_MS = 30_000L
const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15 const val CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 15
private const val DOWNLOADS_QUEUED_WARNING_THRESHOLD = 30
} }
} }
// Arbitrary minimum required space to start a download: 50 MB // Arbitrary minimum required space to start a download: 200 MB
private const val MIN_DISK_SPACE = 50 * 1024 * 1024 private const val MIN_DISK_SPACE = 200L * 1024 * 1024

View File

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

View File

@ -49,7 +49,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
interval.toLong(), interval.toLong(),
TimeUnit.HOURS, TimeUnit.HOURS,
10, 10,
TimeUnit.MINUTES TimeUnit.MINUTES,
) )
.addTag(TAG) .addTag(TAG)
.setConstraints(constraints) .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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.Downloader 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.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -85,31 +86,67 @@ class LibraryUpdateNotifier(private val context: Context) {
Notifications.ID_LIBRARY_PROGRESS, Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder progressNotificationBuilder
.setProgress(total, current, false) .setProgress(total, current, false)
.build() .build(),
)
}
fun showQueueSizeWarningNotification() {
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
setContentTitle(context.getString(R.string.label_warning))
setContentText(context.getString(R.string.notification_size_warning))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
}
context.notificationManager.notify(
Notifications.ID_LIBRARY_SIZE_WARNING,
notificationBuilder.build(),
) )
} }
/** /**
* Shows notification containing update entries that failed with action to open full log. * 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. * @param uri Uri for error log file containing all titles that failed.
*/ */
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) { fun showUpdateErrorNotification(failed: Int, uri: Uri) {
if (errors.isEmpty()) { if (failed == 0) {
return return
} }
context.notificationManager.notify( context.notificationManager.notify(
Notifications.ID_LIBRARY_ERROR, Notifications.ID_LIBRARY_ERROR,
context.notificationBuilder(Notifications.CHANNEL_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)) setContentText(context.getString(R.string.action_show_errors))
setSmallIcon(R.drawable.ic_tachi) setSmallIcon(R.drawable.ic_tachi)
setContentIntent(NotificationReceiver.openErrorLogPendingActivity(context, uri)) 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))
setContentText(context.getString(R.string.learn_more))
setSmallIcon(R.drawable.ic_tachi)
setContentIntent(NotificationHandler.openUrl(context, HELP_SKIPPED_URL))
}
.build(),
) )
} }
@ -139,8 +176,8 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationCompat.BigTextStyle().bigText( NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") { updates.joinToString("\n") {
it.first.title.chop(NOTIF_TITLE_MAX_LEN) it.first.title.chop(NOTIF_TITLE_MAX_LEN)
} },
) ),
) )
} }
} }
@ -155,7 +192,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(getNotificationIntent()) setContentIntent(getNotificationIntent())
setAutoCancel(true) setAutoCancel(true)
} },
) )
// Per-manga notification // Per-manga notification
@ -200,8 +237,8 @@ class LibraryUpdateNotifier(private val context: Context) {
context, context,
manga, manga,
chapters, chapters,
Notifications.ID_NEW_CHAPTERS Notifications.ID_NEW_CHAPTERS,
) ),
) )
// View chapters action // View chapters action
addAction( addAction(
@ -210,8 +247,8 @@ class LibraryUpdateNotifier(private val context: Context) {
NotificationReceiver.openChapterPendingActivity( NotificationReceiver.openChapterPendingActivity(
context, context,
manga, manga,
Notifications.ID_NEW_CHAPTERS Notifications.ID_NEW_CHAPTERS,
) ),
) )
// Download chapters action // Download chapters action
// Only add the action when chapters is within threshold // Only add the action when chapters is within threshold
@ -223,8 +260,8 @@ class LibraryUpdateNotifier(private val context: Context) {
context, context,
manga, manga,
chapters, chapters,
Notifications.ID_NEW_CHAPTERS Notifications.ID_NEW_CHAPTERS,
) ),
) )
} }
} }
@ -251,7 +288,7 @@ class LibraryUpdateNotifier(private val context: Context) {
val formatter = DecimalFormat( val formatter = DecimalFormat(
"#.###", "#.###",
DecimalFormatSymbols() DecimalFormatSymbols()
.apply { decimalSeparator = '.' } .apply { decimalSeparator = '.' },
) )
val displayableChapterNumbers = chapters val displayableChapterNumbers = chapters
@ -303,10 +340,9 @@ class LibraryUpdateNotifier(private val context: Context) {
} }
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 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

@ -18,8 +18,9 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING 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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
@ -74,7 +75,7 @@ class LibraryUpdateService(
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get(), val trackManager: TrackManager = Injekt.get(),
val coverCache: CoverCache = Injekt.get() val coverCache: CoverCache = Injekt.get(),
) : Service() { ) : Service() {
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
@ -138,7 +139,7 @@ class LibraryUpdateService(
true true
} else { } else {
instance?.addMangaToQueue(category?.id ?: -1, target) instance?.addMangaToQueue(category?.id ?: -1)
false false
} }
} }
@ -210,7 +211,7 @@ class LibraryUpdateService(
// Update favorite manga // Update favorite manga
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
addMangaToQueue(categoryId, target) addMangaToQueue(categoryId)
// Destroy service when completed or in case of an error. // Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
@ -235,10 +236,10 @@ class LibraryUpdateService(
* @param category the ID of the category to update, or -1 if no category specified. * @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update. * @param target the target to update.
*/ */
fun addMangaToQueue(categoryId: Int, target: Target) { fun addMangaToQueue(categoryId: Int) {
val libraryManga = db.getLibraryMangas().executeAsBlocking() val libraryManga = db.getLibraryMangas().executeAsBlocking()
var listToUpdate = if (categoryId != -1) { val listToUpdate = if (categoryId != -1) {
libraryManga.filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
} else { } else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
@ -258,16 +259,6 @@ class LibraryUpdateService(
listToInclude.minus(listToExclude) 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 mangaToUpdate = listToUpdate
.distinctBy { it.id } .distinctBy { it.id }
.sortedBy { it.title } .sortedBy { it.title }
@ -277,9 +268,8 @@ class LibraryUpdateService(
.groupBy { it.source } .groupBy { it.source }
.filterKeys { sourceManager.get(it) !is UnmeteredSource } .filterKeys { sourceManager.get(it) !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0 .maxOfOrNull { it.value.size } ?: 0
// TODO: re-enable warning
if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) { if (maxUpdatesFromSource > MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD) {
// toast(R.string.notification_size_warning, Toast.LENGTH_LONG) notifier.showQueueSizeWarningNotification()
} }
} }
@ -297,10 +287,12 @@ class LibraryUpdateService(
val progressCount = AtomicInteger(0) val progressCount = AtomicInteger(0)
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>() val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
val skippedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false) val hasDownloads = AtomicBoolean(false)
val loggedServices by lazy { trackManager.services.filter { it.isLogged } } val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get() val currentUnreadUpdatesCount = preferences.unreadUpdatesCount().get()
val restrictions = preferences.libraryUpdateMangaRestriction().get()
withIOContext { withIOContext {
mangaToUpdate.groupBy { it.source } mangaToUpdate.groupBy { it.source }
@ -319,19 +311,33 @@ class LibraryUpdateService(
manga, manga,
) { manga -> ) { manga ->
try { try {
val (newChapters, _) = updateManga(manga) when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
if (newChapters.isNotEmpty()) { skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads.set(true)
} }
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 if (newChapters.isNotEmpty()) {
newUpdates.add( if (manga.shouldDownloadNewChapters(db, preferences)) {
manga to newChapters.sortedByDescending { ch -> ch.source_order } downloadChapters(manga, newChapters)
.toTypedArray() 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) { } catch (e: Throwable) {
val errorMessage = when (e) { val errorMessage = when (e) {
@ -374,10 +380,13 @@ class LibraryUpdateService(
if (failedUpdates.isNotEmpty()) { if (failedUpdates.isNotEmpty()) {
val errorFile = writeErrorFile(failedUpdates) val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification( notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title }, failedUpdates.size,
errorFile.getUriCompat(this) errorFile.getUriCompat(this),
) )
} }
if (skippedUpdates.isNotEmpty()) {
notifier.showUpdateSkippedNotification(skippedUpdates.size)
}
} }
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) { private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -525,7 +534,7 @@ class LibraryUpdateService(
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.get(),
mangaToUpdate.size mangaToUpdate.size,
) )
block(manga) block(manga)
@ -539,7 +548,7 @@ class LibraryUpdateService(
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.get(),
mangaToUpdate.size mangaToUpdate.size,
) )
} }

View File

@ -6,8 +6,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.ui.main.MainActivity 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 * 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 flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
action = MainActivity.SHORTCUT_DOWNLOADS 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 context context of application
* @param file file containing image * @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 intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "image/*") setDataAndType(uri, "image/*")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION 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) setDataAndType(uri, ExtensionInstaller.APK_MIME)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION 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( shareImage(
context, context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!, intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
) )
// Delete image from path and dismiss notification // Delete image from path and dismiss notification
ACTION_DELETE_IMAGE -> ACTION_DELETE_IMAGE ->
deleteImage( deleteImage(
context, context,
intent.getStringExtra(EXTRA_FILE_LOCATION)!!, intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
) )
// Share backup file // Share backup file
ACTION_SHARE_BACKUP -> ACTION_SHARE_BACKUP ->
@ -74,11 +74,11 @@ class NotificationReceiver : BroadcastReceiver() {
context, context,
intent.getParcelableExtra(EXTRA_URI)!!, intent.getParcelableExtra(EXTRA_URI)!!,
"application/x-protobuf+gzip", "application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
) )
ACTION_CANCEL_RESTORE -> cancelRestore( ACTION_CANCEL_RESTORE -> cancelRestore(
context, context,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
) )
// Cancel library update and dismiss notification // Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS) ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
@ -87,7 +87,7 @@ class NotificationReceiver : BroadcastReceiver() {
openChapter( openChapter(
context, context,
intent.getLongExtra(EXTRA_MANGA_ID, -1), intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1) intent.getLongExtra(EXTRA_CHAPTER_ID, -1),
) )
} }
// Mark updated manga chapters as read // Mark updated manga chapters as read
@ -120,7 +120,7 @@ class NotificationReceiver : BroadcastReceiver() {
context, context,
intent.getParcelableExtra(EXTRA_URI)!!, intent.getParcelableExtra(EXTRA_URI)!!,
"text/plain", "text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1) intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
) )
} }
} }
@ -461,7 +461,7 @@ class NotificationReceiver : BroadcastReceiver() {
context: Context, context: Context,
manga: Manga, manga: Manga,
chapters: Array<Chapter>, chapters: Array<Chapter>,
groupId: Int groupId: Int,
): PendingIntent { ): PendingIntent {
val newIntent = Intent(context, NotificationReceiver::class.java).apply { val newIntent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_MARK_AS_READ action = ACTION_MARK_AS_READ
@ -483,7 +483,7 @@ class NotificationReceiver : BroadcastReceiver() {
context: Context, context: Context,
manga: Manga, manga: Manga,
chapters: Array<Chapter>, chapters: Array<Chapter>,
groupId: Int groupId: Int,
): PendingIntent { ): PendingIntent {
val newIntent = Intent(context, NotificationReceiver::class.java).apply { val newIntent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_DOWNLOAD_CHAPTER action = ACTION_DOWNLOAD_CHAPTER

View File

@ -26,8 +26,11 @@ object Notifications {
private const val GROUP_LIBRARY = "group_library" private const val GROUP_LIBRARY = "group_library"
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel" const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
const val ID_LIBRARY_PROGRESS = -101 const val ID_LIBRARY_PROGRESS = -101
const val ID_LIBRARY_SIZE_WARNING = -103
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel" const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102 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. * Notification channel and ids used by the downloader.
@ -114,7 +117,7 @@ object Notifications {
buildNotificationChannelGroup(GROUP_APK_UPDATES) { buildNotificationChannelGroup(GROUP_APK_UPDATES) {
setName(context.getString(R.string.label_recent_updates)) setName(context.getString(R.string.label_recent_updates))
}, },
) ),
) )
notificationService.createNotificationChannelsCompat( notificationService.createNotificationChannelsCompat(
@ -132,6 +135,11 @@ object Notifications {
setGroup(GROUP_LIBRARY) setGroup(GROUP_LIBRARY)
setShowBadge(false) 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) { buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
setName(context.getString(R.string.channel_new_chapters)) setName(context.getString(R.string.channel_new_chapters))
}, },
@ -175,7 +183,7 @@ object Notifications {
setGroup(GROUP_APK_UPDATES) setGroup(GROUP_APK_UPDATES)
setName(context.getString(R.string.channel_ext_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 filterUnread = "pref_filter_library_unread"
const val filterStarted = "pref_filter_library_started"
const val filterCompleted = "pref_filter_library_completed" const val filterCompleted = "pref_filter_library_completed"
const val filterTracked = "pref_filter_library_tracked" 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_ONLY_ON_WIFI = "wifi"
const val DEVICE_CHARGING = "ac" const val DEVICE_CHARGING = "ac"
const val MANGA_ONGOING = "manga_ongoing" const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_FULLY_READ = "manga_fully_read" 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. * This class stores the values for the preferences in the application.
@ -55,16 +56,22 @@ object PreferenceValues {
LOWEST(47), LOWEST(47),
} }
enum class TabletUiMode { enum class TabletUiMode(val titleResId: Int) {
AUTOMATIC, AUTOMATIC(R.string.automatic_background),
ALWAYS, ALWAYS(R.string.lock_always),
LANDSCAPE, LANDSCAPE(R.string.landscape),
NEVER, NEVER(R.string.lock_never),
} }
enum class ExtensionInstaller { enum class ExtensionInstaller(val titleResId: Int) {
LEGACY, LEGACY(R.string.ext_installer_legacy),
PACKAGEINSTALLER, PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
SHIZUKU, 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

@ -34,13 +34,13 @@ class PreferencesHelper(val context: Context) {
private val defaultDownloadsDir = File( private val defaultDownloadsDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name), context.getString(R.string.app_name),
"downloads" "downloads",
).toUri() ).toUri()
private val defaultBackupDir = File( private val defaultBackupDir = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.getString(R.string.app_name), context.getString(R.string.app_name),
"backup" "backup",
).toUri() ).toUri()
fun startScreen() = prefs.getInt(Keys.startScreen, 1) 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 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) fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
@ -67,12 +67,12 @@ class PreferencesHelper(val context: Context) {
fun themeMode() = flowPrefs.getEnum( fun themeMode() = flowPrefs.getEnum(
"pref_theme_mode_key", "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( fun appTheme() = flowPrefs.getEnum(
"pref_app_theme", "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) 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 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 cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0) 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 pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", 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 libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
fun libraryUpdateDeviceRestriction() = flowPrefs.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI)) 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 showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0) 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 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 filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value) fun 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( fun extensionInstaller() = flowPrefs.getEnum(
"extension_installer", "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) fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)

View File

@ -0,0 +1,151 @@
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 eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority
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 || 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(context.getString(R.string.error_saving_picture))
try {
data().use { input ->
@Suppress("BlockingMethodInNonBlockingContext")
context.contentResolver.openOutputStream(picture, "w").use { output ->
input.copyTo(output!!)
}
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
throw IOException(context.getString(R.string.error_saving_picture))
}
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 | status
|} |}
|} |}
|""".trimMargin() |
""".trimMargin()
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
putJsonObject("variables") { putJsonObject("variables") {
@ -55,8 +56,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
body = payload.toString().toRequestBody(jsonMime) body = payload.toString().toRequestBody(jsonMime),
) ),
) )
.await() .await()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -84,7 +85,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|progress |progress
|} |}
|} |}
|""".trimMargin() |
""".trimMargin()
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
putJsonObject("variables") { putJsonObject("variables") {
@ -127,7 +129,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|} |}
|} |}
|""".trimMargin() |
""".trimMargin()
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
putJsonObject("variables") { putJsonObject("variables") {
@ -137,8 +140,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
body = payload.toString().toRequestBody(jsonMime) body = payload.toString().toRequestBody(jsonMime),
) ),
) )
.await() .await()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -193,7 +196,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|} |}
|} |}
|""".trimMargin() |
""".trimMargin()
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
putJsonObject("variables") { putJsonObject("variables") {
@ -204,8 +208,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
body = payload.toString().toRequestBody(jsonMime) body = payload.toString().toRequestBody(jsonMime),
) ),
) )
.await() .await()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -238,15 +242,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|} |}
|} |}
|} |}
|""".trimMargin() |
""".trimMargin()
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
} }
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
body = payload.toString().toRequestBody(jsonMime) body = payload.toString().toRequestBody(jsonMime),
) ),
) )
.await() .await()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -255,7 +260,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val viewer = data["Viewer"]!!.jsonObject val viewer = data["Viewer"]!!.jsonObject
Pair( Pair(
viewer["id"]!!.jsonPrimitive.int, 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["format"]!!.jsonPrimitive.content.replace("_", "-"),
struct["status"]!!.jsonPrimitive.contentOrNull ?: "", struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
parseDate(struct, "startDate"), 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, struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"), parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"), parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject) jsonToALManga(struct["media"]!!.jsonObject),
) )
} }
@ -292,7 +297,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
date.set( date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int, struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1, struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
) )
date.timeInMillis date.timeInMillis
} catch (_: Exception) { } catch (_: Exception) {

View File

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

View File

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

View File

@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
data class Avatar( data class Avatar(
val large: String? = "", val large: String? = "",
val medium: 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( authClient.newCall(
POST( POST(
"$apiUrl/subject/${track.media_id}/update/watched_eps", "$apiUrl/subject/${track.media_id}/update/watched_eps",
body = body body = body,
) ),
).await() ).await()
track track
@ -168,7 +168,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("client_secret", clientSecret) .add("client_secret", clientSecret)
.add("code", code) .add("code", code)
.add("redirect_uri", redirectUrl) .add("redirect_uri", redirectUrl)
.build() .build(),
) )
companion object { companion object {
@ -201,7 +201,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.add("client_secret", clientSecret) .add("client_secret", clientSecret)
.add("refresh_token", token) .add("refresh_token", token)
.add("redirect_uri", redirectUrl) .add("redirect_uri", redirectUrl)
.build() .build(),
) )
} }
} }

View File

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

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