Compare commits

...

205 Commits

Author SHA1 Message Date
Jobobby04 263c0fae8c Release v1.8.3 2022-04-22 19:39:42 -04:00
Howard Wu 7756f25312 Add Simplified Chinese translation (#584)
* Add Simplified Chinese translation

Work In Program
Part 1

* Add more translate

* Add more translate

* Add more translate

* Add more translate

* Fix

* Minor changes

* Fix some strings

* Fix some strings
2022-04-22 19:38:51 -04:00
Jobobby04 6a0b523e86 Revert history Compose/SQLDelight changes 2022-04-22 19:27:15 -04:00
arkon 070e2d94c7 Temporarily remove chapter name cleaning
To be added back in a more consistent manner later around the app. Probably when more things are Compose-y with less repetition.

(cherry picked from commit c0214103a9)
2022-04-22 19:23:45 -04:00
arkon 743482dfd2 Add advanced setting to clear WebView data
(cherry picked from commit 2b76a97989)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-04-22 19:23:37 -04:00
Andreas f6b7f9e29f Enable verbose logging in dev flavor by default (#6979)
(cherry picked from commit 9d77052d9c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-04-22 19:22:12 -04:00
Andreas 5c9f98bff1 Add indexes to creational tables (#6974)
(cherry picked from commit b4981058a2)
2022-04-22 19:21:09 -04:00
arkon d375d7d8c8 Lift Compose theme to abstract controller
(cherry picked from commit 032aa64195)
2022-04-22 19:21:01 -04:00
arkon a88bcb0fa2 Simplify history item description building
(cherry picked from commit 7c8e8317a8)
2022-04-22 19:20:54 -04:00
arkon 5512c6eb79 Add abstract ComposeController
(cherry picked from commit eb1cfc4cd4)
2022-04-22 19:20:46 -04:00
arkon 97e4b0e248 Add placeholder color for Compose manga covers
(cherry picked from commit f1e5cccee7)
2022-04-22 19:20:39 -04:00
arkon 99a94150ea Default auto backups to 2
(cherry picked from commit bc2ed763bd)
2022-04-22 19:20:32 -04:00
Jobobby04 26b30adf4a Migrate saved search and feed saved search to SQLDelight 2022-04-22 19:19:50 -04:00
Jobobby04 4a115785eb Add SY specific queries to sqldelight files 2022-04-22 19:16:48 -04:00
Andreas a8cb77cc7e Migrate History screen database calls to SQLDelight (#6933)
* Migrate History screen database call to SQLDelight

- Move all migrations to SQLDelight
- Move all tables to SQLDelight

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>

* Changes from review comments

* Add adapters to database

* Remove logging of database version in App

* Change query name for paging source queries

* Update migrations

* Make SQLite Callback handle migration

- To ensure it updates the database

* Use SQLDelight Schema version for Callback database version

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>
(cherry picked from commit b1f46ed830)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt
#	build.gradle.kts
2022-04-22 10:08:31 -04:00
arkon c44c37383d Make links in new update dialog clickable
Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
(cherry picked from commit 6c1565a7d4)
2022-04-21 17:07:12 -04:00
arkon 8e72394910 Replace ignore button in new update dialog with link to GitHub page
Not enough room for 3 buttons. Users can still tap outside or back out of the dialog if they want to ignore it.

(cherry picked from commit 2ca6b655ad)
2022-04-21 17:06:57 -04:00
arkon e5349a3d33 Update junrar
(cherry picked from commit a83a481ac8)
2022-04-21 17:06:50 -04:00
arkon e6aa6f02e4 Move chapter name cleaning logic to holder (fixes #6955)
(cherry picked from commit 65a8b63b3b)
2022-04-21 17:06:39 -04:00
Andreas 231c75df65 Fix AppBar not unlifting when scrolling using ComposeView (#6952)
(cherry picked from commit b20ca36db9)
2022-04-21 17:06:31 -04:00
arkon 08c2bfd263 Show better error message when empty backup creation is attempted (closes #6941)
(cherry picked from commit 189f92d7e8)
2022-04-21 17:06:25 -04:00
arkon 33bdf011b4 Increase default OkHttp call timeout to 2 minutes
Which is still stupidly high, but maybe it'll be lenient enough for certain people.

(cherry picked from commit cdd4ec6233)
2022-04-21 17:06:18 -04:00
arkon 26deb46219 Show parsed Markdown for new version info (closes #6940)
(cherry picked from commit ef1bb4e800)
2022-04-21 17:06:11 -04:00
Andreas 45bfd5f72c Migrate History screen to Compose (#6922)
* Migrate History screen to Compose

- Migrate screen
- Strip logic from presenter into use cases and repository
- Setup for other screen being able to migrate to Compose with Theme

* Changes from review comments

(cherry picked from commit c475acd1ea)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	settings.gradle.kts
2022-04-21 17:06:03 -04:00
CrepeTF 32d81eb1fa Add elevation to navigation rails (#6947)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit 7d50d7ff52)
2022-04-21 17:01:34 -04:00
Jobobby04 4309b4c0d7 Release v1.8.2 2022-04-15 18:25:54 -04:00
Jozef Hollý 2c3f7f5206 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>
(cherry picked from commit ec3a227a02)
2022-04-15 17:52:11 -04:00
arkon d670d29169 Always remove manga title from if it prefixes chapter names (related to #6913)
(cherry picked from commit 89decf3474)
2022-04-15 17:52:00 -04:00
arkon a4c61e49f4 Limit package name overriding to Android 8+ (related to #6846)
(cherry picked from commit 0b2794e843)
2022-04-15 17:51:53 -04:00
arkon 3d00e85dc2 Bump Material Components
(cherry picked from commit 554dfb5874)
2022-04-15 17:51:47 -04:00
arkon 46f39c24b0 Update F-Droid migration guide link
(cherry picked from commit 9c30fa1da3)
2022-04-15 17:51:40 -04:00
arkon 418da04411 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

(cherry picked from commit e81bd61e24)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2022-04-15 17:51:23 -04:00
arkon 2d9cd81b62 Set network call timeout to 90 seconds (instead of infinite)
(cherry picked from commit 7a0b54bb38)
2022-04-15 17:50:44 -04:00
arkon 2bd161d5a2 Rollback to stable OkHttp
There's some weird crashes related to it. Happy Eyeballs will return once we upgrade again.

(cherry picked from commit f060daf8c4)
2022-04-15 17:50:35 -04:00
arkon af25e0e770 Minor cleanup
(cherry picked from commit f16fb4e1e4)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
2022-04-15 17:50:26 -04:00
arkon 7cf5208000 Avoid crashing if picture can't be saved (related to #6905)
(cherry picked from commit 5da2c82f47)
2022-04-15 17:49:44 -04:00
FourTOne5 12bda2a966 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>
(cherry picked from commit d443245d66)
2022-04-15 17:49:32 -04:00
arkon 69f524717a Add clear cookies option to WebView menu
(cherry picked from commit 2a070c0b1e)
2022-04-15 17:48:58 -04:00
arkon c6972b04d2 Update ACRA
(cherry picked from commit 7b5106d206)
2022-04-15 17:48:50 -04:00
arkon 1be153e51c Show different update notification for F-Droid installations
(cherry picked from commit 821d9cdb02)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt
2022-04-15 17:48:41 -04:00
arkon 0a2684a1fe Move learn more text in skipped entries notification to main content
Because people apparently don't realize they can tap actions

(cherry picked from commit 28575936d3)
2022-04-15 17:47:44 -04:00
arkon b16f91571d 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.

(cherry picked from commit 83a04da4a0)
2022-04-15 17:47:35 -04:00
jmir1 a55964ee3d Fix cover sharing error string (#6911)
(cherry picked from commit 0894b1394f)
2022-04-15 17:47:27 -04:00
arkon f473415968 Remove build flavor checks for update warnings
"stable" was invalid anyway, it should've been "release"

(cherry picked from commit eb33d3c991)
2022-04-15 17:47:14 -04:00
arkon 78754a96d6 Update Coil
(cherry picked from commit d7f01abf3a)
2022-04-15 17:47:06 -04:00
arkon 1992a2a4c4 Update ACRA
(cherry picked from commit 80635343ae)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
2022-04-15 17:46:56 -04:00
arkon 897eed3ba4 Gate update/download warnings to non-stable flavors
(cherry picked from commit 4ecde9fc39)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2022-04-15 17:46:09 -04:00
arkon 5b2e307f92 Update to AGP 7.1.3
(cherry picked from commit 445ee274c5)
2022-04-15 17:45:30 -04:00
CrepeTF d21dac8a2d Tweaks to migration sheet (#566)
* Added divider to top of migrate button + adjusted top margin

* Migration sheet now opens fully when initialized

Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-04-09 19:39:06 -04:00
Jozef Hollý 0a7933856c 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>
(cherry picked from commit f2bdc514e8)
2022-04-09 19:37:51 -04:00
Jobobby04 11f31769ac Formatting 2022-04-09 19:37:39 -04:00
arkon f3e17edd6c Remove reader tapping option in favor of disabled nav layouts
(cherry picked from commit 2dfafa387b)

# Conflicts:
#	app/build.gradle.kts
2022-04-09 19:37:09 -04:00
arkon 0a110d149a Remove some dead code
(cherry picked from commit 7318f4f5dd)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchPresenter.kt
2022-04-09 19:33:23 -04:00
manatails 8a1d277630 Add option to disable navigation layout (#6876)
(cherry picked from commit 175b77fe6f)
2022-04-09 19:15:16 -04:00
arkon 8244ca9898 Ensure media store scan is triggered after saving an image (fixes #6808)
(cherry picked from commit 346652e508)
2022-04-09 19:14:52 -04:00
arkon e98567a86b Update linter
(cherry picked from commit f0eb42e72d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/Backup.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/models/BackupManga.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt
#	app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
#	app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/base/changehandler/OneWayFadeChangeHandler.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/sources/SelectionHeader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourcePresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderPage.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
2022-04-09 19:11:54 -04:00
Andreas e0c1e56588 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

(cherry picked from commit 37100f0937)
2022-04-09 19:05:40 -04:00
Ivan Iskandar 3f7302c4eb 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

(cherry picked from commit ac980a4dbf)
2022-04-09 19:05:32 -04:00
arkon b25da34b64 Remove kotlin.compiler.execution.strategy config
(cherry picked from commit a8b53499af)
2022-04-09 19:05:24 -04:00
arkon c4b67c4eb1 Bump to Gradle 7.4.2
(cherry picked from commit a8aeae329e)
2022-04-09 19:05:18 -04:00
arkon 41944164e5 Bump dependencies
(cherry picked from commit 52911539b8)
2022-04-09 19:05:11 -04:00
Andreas a8a6effd86 Write library cover to library cover cache (#6883)
(cherry picked from commit 3026ff241b)
2022-04-09 19:05:02 -04:00
Ivan Iskandar 6a45a91a50 MangaCoverFetcher: Don't close network response (#6882)
(cherry picked from commit 2466a079d5)
2022-04-09 19:04:56 -04:00
Jobobby04 9e78f4f0f1 Most likely fix clear database with keeping read 2022-04-09 19:04:19 -04:00
Jobobby04 88bccfc015 Minor cleanup 2022-04-03 17:36:28 -04:00
Alessandro Jean ecc0082db0 Add missing percent placeholder in some singular strings. (#6855)
(cherry picked from commit ed9fdf49e2)
2022-04-03 12:03:21 -04:00
arkon 3648ef4397 Update WebView requester package name
https://github.com/tachiyomiorg/tachiyomi/issues/6781#issuecomment-1086665483
(cherry picked from commit 668d962233)
2022-04-03 12:03:12 -04:00
arkon 989119af17 Override X-Requested-With header value in WebView requests (closes #6781)
(cherry picked from commit 996f770935)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
2022-04-03 12:03:04 -04:00
arkon 4482ab4a68 Update Coil
(cherry picked from commit 041a6dd919)
2022-04-03 12:02:04 -04:00
Ivan Iskandar a700c1a230 Base activities cleanup (#6848)
* secure delegate

* theming delegate

(cherry picked from commit dbad60d03b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/security/SecureActivityDelegate.kt
2022-04-03 12:01:56 -04:00
CrepeTF b487e29059 Remove source filter sheet solid background (#6850)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit 27a60423dc)
2022-04-03 11:50:11 -04:00
CrepeTF 311b1c23e5 Stop global search items from clipping (#6851)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit 5a37d38a84)
2022-04-03 11:50:03 -04:00
CrepeTF 5dfc855ade Removed scrollbar on long theme item titles (#6852)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit 6f566e67d5)
2022-04-03 11:49:54 -04:00
Andreas d149e3186a Fix DST issue (#6831)
(cherry picked from commit dd490f2ac9)
2022-04-03 11:49:45 -04:00
Ivan Iskandar c377afac2e MangaCoverFetcher: Use source's header for network request (#6847)
(cherry picked from commit 5409af0a6c)
2022-04-03 11:49:39 -04:00
Román 9878e0025a [RU] Translations (#562)
* [RU] Translations

* [RU] Fix
2022-03-29 14:05:39 -04:00
e-shl 28c3511984 [RU] Translations and fix origin string (#560)
* [RU] Translations and fix origin string

* -
2022-03-29 13:29:42 -04:00
Jobobby04 7b7e625f57 Fix source feed manga click 2022-03-28 09:17:33 -04:00
Jobobby04 2207d9ffa4 Properly check if the source supports latest 2022-03-27 20:35:56 -04:00
Jobobby04 ceca8207ad Forgot this 2022-03-27 20:29:14 -04:00
Jobobby04 c67b7092fb Cleanup unused files and strings 2022-03-27 20:24:13 -04:00
arkon 5d1d5778ad Force default browser for tracker logins
To avoid potentially opening up third party apps, which aren't useful for handling OAuth login flows.

(cherry picked from commit 0ed0d903cc)
2022-03-27 20:16:11 -04:00
arkon 46bb17ce81 Fix clear database selection toggling (fixes #6807)
(cherry picked from commit 85be4c492d)
2022-03-27 20:16:01 -04:00
arkon 704b3b0508 Stop using custom tabs (closes #6821)
(cherry picked from commit c06ad8b87e)
2022-03-27 20:15:52 -04:00
arkon d98e0c5f68 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.

(cherry picked from commit b89acb5853)
2022-03-27 20:14:22 -04:00
arkon 30f71b126f Update dependencies
(cherry picked from commit 7890511a53)
2022-03-27 20:14:15 -04:00
Franco Olivera 0bff96e0d7 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"

(cherry picked from commit 3aa4e6eb93)
2022-03-27 20:14:07 -04:00
Andreas 6ef1f566ec Fix filename not having chapter title and page when sharing (#6827)
(cherry picked from commit f8eb9f94f4)
2022-03-27 20:13:58 -04:00
Jozef Hollý 18c1234dfc 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>
(cherry picked from commit c581b9eeb9)
2022-03-27 20:13:48 -04:00
Ivan Iskandar b3e8214a20 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.

(cherry picked from commit ffd9c6995a)
2022-03-27 20:13:40 -04:00
Ivan Iskandar eb533c4498 Fix extension update badge reset when app resumed (#6822)
(cherry picked from commit ef600c0956)
2022-03-27 20:13:28 -04:00
arkon d8179f992e Fix off by 1 dates (fixes #6791)
(cherry picked from commit 5c0a43e8d6)
2022-03-27 20:13:18 -04:00
arkon ab292d6c71 Update Material Components
(cherry picked from commit 8e332dba30)
2022-03-27 20:13:05 -04:00
Andreas a081b88a5b 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+

(cherry picked from commit cd07027192)
2022-03-27 20:12:22 -04:00
Jobobby04 d0e9d24f6f Add feed to the combined sources menus 2022-03-27 20:09:39 -04:00
Jobobby04 6a41d96ddf Replace Latest tab with Feed 2022-03-27 18:45:14 -04:00
Jobobby04 5d330c4f75 Migrate saved searches to the db 2022-03-27 15:00:18 -04:00
arkon 1ebcfc53d4 Add support for Happy Eyeballs
(cherry picked from commit da2b30268a)
2022-03-20 13:09:49 -04:00
Andreas 7569955f9e 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

(cherry picked from commit 1163aa4e4e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/AppModule.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
2022-03-20 13:09:33 -04:00
Ivan Iskandar f3f74264c3 Add cover error drawable (#6782)
(cherry picked from commit ddb856edc7)
2022-03-20 12:38:28 -04:00
arkon 8a32db268e Avoid crashing when global search encounters a NoClassDefFoundError
(cherry picked from commit 9c426bc216)
2022-03-20 12:38:21 -04:00
arkon ddf9a81335 Require WebView v95+
(cherry picked from commit 382852d0bd)
2022-03-20 12:38:13 -04:00
Jobobby04 0ea0cd5fe3 Fix scanlator filter display 2022-03-20 12:37:46 -04:00
nicki 75a99cbc5d Save combined image now respects folderPerManga (#543) 2022-03-13 19:24:27 -04:00
Jobobby04 d31d99a416 Rewrite IndexPresenter in flow 2022-03-13 19:23:03 -04:00
Jobobby04 a5e691271b Minor cleanup 2022-03-13 19:22:43 -04:00
Sahaab 4a96b6ac77 Added reverse portrait reader rotation
(cherry picked from commit 87ae86e1be)
2022-03-13 19:18:57 -04:00
quangkieu 09bef11e6b Avoid throw as it is slow expensive operations
(cherry picked from commit 9547311d7d)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2022-03-13 19:18:50 -04:00
arkon 1cba2536af Support Android 13 themed app icon
(cherry picked from commit 267ecce958)
2022-03-13 19:14:05 -04:00
Ivan Iskandar e9960c0dd8 ReaderActivity: Reduce anim duration when launched from resume FAB (#6762)
From enter 500ms exit 400ms
To both 350ms

(cherry picked from commit fae43fedfa)
2022-03-13 19:13:53 -04:00
arkon 1ad2146d6a Disable app cache WebView (is a deprecated web API and is being removed in Android 13)
(cherry picked from commit c447022092)
2022-03-13 19:13:44 -04:00
arkon 56d6964db9 Split out global library update skipped entries into separate notification (closes #6722)
(cherry picked from commit 56042ad0b6)
2022-03-13 19:13:34 -04:00
arkon 324280aed4 Avoid potentially deleting the entire backups folder
(cherry picked from commit 45da036789)
2022-03-13 19:13:24 -04:00
arkon 0b2dabc7fa Copy raw description on long tap (fixes #6557)
(cherry picked from commit b47b702a52)
2022-03-13 19:13:15 -04:00
Ivan Iskandar 4a627ea359 Change cover placeholder (#6756)
(cherry picked from commit 869424cd16)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/widget/StateImageViewTarget.kt
2022-03-13 19:13:05 -04:00
arkon 01b8256daf Minor cleanup
(cherry picked from commit b9fd01315b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
2022-03-07 12:35:08 -05:00
arkon 3e27e8943b Add shortcut to edit categories screen from category setting dialog (closes #6280)
(cherry picked from commit a72098b862)
2022-03-07 12:33:06 -05:00
Andreas d2972c7c5a 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

(cherry picked from commit 86016de6cb)

# Conflicts:
#	app/build.gradle.kts
2022-03-07 12:32:58 -05:00
1831553190 7c2283c962 Fixed the wrong offset (#6704)
(cherry picked from commit 592b9fedb9)
2022-03-07 12:31:57 -05:00
arkon 2273a50920 Use same name for manual backup job tag and work name
(cherry picked from commit d06984e3a3)
2022-03-07 12:31:50 -05:00
Jobobby04 2e8393ea30 Remove unused version number 2022-03-04 19:12:40 -05:00
Jobobby04 621c083b79 Update dependencies 2022-03-04 17:00:39 -05:00
arkon 70b3f1bc1f Update AGP and Gradle
(cherry picked from commit 6b55ee250d)
2022-03-04 16:46:29 -05:00
Ivan Iskandar 1f8072f18b 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

(cherry picked from commit 10eef282fa)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt
2022-03-04 16:45:57 -05:00
Jobobby04 4b1d6400a4 Move SY dependencies to version catalogs 2022-03-04 16:34:21 -05:00
Andreas 1df1a331dd Use Version Catalog & clean up Gradle files (#6728)
(cherry picked from commit f312936629)

# Conflicts:
#	app/build.gradle.kts
#	build.gradle.kts
#	settings.gradle.kts
2022-03-04 16:33:42 -05:00
Ivan Iskandar 7918b3b26b 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

(cherry picked from commit d53bb4c337)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateService.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBackupController.kt
2022-03-04 16:13:24 -05:00
Riztard Lanthorn bf63af8137 Remove unused string (#6726)
* change wording if update restriction is off

from
  Only update: none
to
  Restrictions: none

* remove unused string

(cherry picked from commit 1a605e27bc)
2022-03-04 16:06:07 -05:00
Jobobby04 bc1274008d Delete duplicate history on merge 2022-03-04 12:27:18 -05:00
FourTOne5 2026f34956 Adjust mark as unread and mark previous as read action visibility (#6703)
(cherry picked from commit 08ee858f64)
2022-03-03 11:29:26 -05:00
arkon d8c295a293 [skip ci] Move auto-closer rules
(cherry picked from commit af70fe3e7e)
2022-03-03 11:28:59 -05:00
arkon 5460a0d563 Update Material Components
(cherry picked from commit 29c5c0af50)
2022-03-03 11:28:52 -05:00
arkon b46a92e613 Adjust badge font weights
(cherry picked from commit 9420b750d2)
2022-03-03 11:28:43 -05:00
啊o额iu鱼 1803f49732 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,

(cherry picked from commit 6f5328f663)
2022-03-03 11:28:35 -05:00
FourTOne5 a7d7aa1ec5 Add Prerequisites and Getting help to Contributing.md (#6682)
(cherry picked from commit 90214d02d7)
2022-03-03 11:28:29 -05:00
Jobobby04 8185b91f11 Fix HBrowse new galleries 2022-02-26 12:10:37 -05:00
Jobobby04 0bd09d532d Only show scanlator filter if scanlator count is 2 or over 2022-02-22 21:35:23 -05:00
Jobobby04 b0f5d4d1ce Cleanup group by code a bit 2022-02-22 21:34:24 -05:00
Jobobby04 b1f7165ad7 Cleanup pager page change handling 2022-02-19 19:18:59 -05:00
Jobobby04 574dd17906 Fix possible pager bug 2022-02-19 17:05:19 -05:00
Gauthier 1231dd1496 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

(cherry picked from commit 2f07f226b8)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
2022-02-19 17:02:35 -05:00
Ivan Iskandar d343964fa7 Restore bottom nav position earlier after being recreated (#6648)
(cherry picked from commit a8ad19a89d)
2022-02-19 16:58:34 -05:00
Román a64cd44d61 Side padding: Added missing percentage (#6668)
(cherry picked from commit 57c07250fd)
2022-02-19 16:58:27 -05:00
arkon 5777db5509 Reword library update restrictions setting and surface skipped entries in error notification/log
(cherry picked from commit 4a3e4a7c5c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt
2022-02-19 16:58:17 -05:00
Jobobby04 2bb9e596ba Minor cleanup 2022-02-19 16:48:41 -05:00
Jobobby04 07e28ca5c2 Proper fix for no-title grid crash 2022-02-13 20:22:17 -05:00
Jobobby04 b58fb48a20 Fix no-title grid crash in source browse 2022-02-13 20:05:59 -05:00
Jobobby04 dcd8c3a378 Fix play button being in the wrong spot in cover-only grid 2022-02-13 20:05:31 -05:00
Jobobby04 5f5dea905c Dont do database stuff on UI thread 2022-02-13 15:54:04 -05:00
Jobobby04 af7b0ead98 Fix rounded corners in migration items 2022-02-13 15:53:43 -05:00
Jobobby04 76b153346f Remove migration sheet header 2022-02-13 12:24:14 -05:00
Jobobby04 cf49b5e37a Fix play button location when language badge is not enabled 2022-02-13 11:57:28 -05:00
arkon 286844e56d Avoid some crashes if router backstack is empty for whatever reason
(cherry picked from commit c284a23afb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
2022-02-13 11:46:30 -05:00
Ivan Iskandar c293fd61b1 Grid items optimizations (#6641)
Use ConstraintLayout for ez size ratio calculation and merge cover-only view
holder with compact's

(cherry picked from commit fad1449de3)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryComfortableGridHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCompactGridHolder.kt
#	app/src/main/res/layout/source_comfortable_grid_item.xml
#	app/src/main/res/layout/source_compact_grid_item.xml
2022-02-13 11:45:15 -05:00
FourTOne5 a12758579d 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>
(cherry picked from commit f18d161eaf)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/LibraryMangaGetResolver.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt
2022-02-13 11:16:49 -05:00
Jobobby04 3b56bcfbba Most likely fix NO_TITLE_GRID crash 2022-02-13 10:56:55 -05:00
Román 819d57155a [RU] Translations (#527) 2022-02-12 23:09:47 -05:00
arkon ad9f063716 Fix Quad9 DoH setting
(cherry picked from commit d698d03521)
2022-02-12 22:32:51 -05:00
OncePunchedMan 8a55027f67 Add Quad9 DOH provider (#6638)
* add quad9 as new doh provider

* add ipv6 addresses to google doh

* revert changes to import

(cherry picked from commit d8c8d7c588)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-02-12 22:32:32 -05:00
arkon 76e3f0e5cb Consistent divider colour
(cherry picked from commit 9120e82517)
2022-02-12 22:31:46 -05:00
arkon b42a0b135d Update action_display_cover_only_grid string
(cherry picked from commit e214746536)
2022-02-12 22:31:39 -05:00
Jozef Hollý 7e3ed2f00e 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>
(cherry picked from commit 142396400c)
2022-02-12 22:31:22 -05:00
CrepeTF 8f90aa12fb 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>
(cherry picked from commit 51d48bdde6)
2022-02-12 22:31:09 -05:00
Mica 5bad65c027 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>
(cherry picked from commit 44b055c019)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/setting/DisplayModeSetting.kt
2022-02-12 22:30:54 -05:00
arkon f133ddb14e Rename extension function to avoid confusion with androidx function
(cherry picked from commit 790d7b9170)
2022-02-12 12:12:57 -05:00
Gauthier 026a1116ee 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

(cherry picked from commit d8719ceee9)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerConfig.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
2022-02-12 12:12:39 -05:00
Felix Kaiser 0adab16fea 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

(cherry picked from commit 71ddb16574)
2022-02-12 12:02:21 -05:00
Ivan Iskandar 01dbe7f850 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

(cherry picked from commit 2932ed670f)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
2022-02-12 12:02:06 -05:00
arkon 198a59cc2d Update dependencies
(cherry picked from commit ae2a6a3d4f)
2022-02-12 11:56:04 -05:00
arkon 3ca70543d1 Update AGP for Android Studio Bumblebee | 2021.1.1 Patch 1
(cherry picked from commit 30061ada58)
2022-02-12 11:55:57 -05:00
Vetle Ledaal 7bc436dce2 [skip ci] docs: update app update checker link (#6619)
(cherry picked from commit a131e28b60)
2022-02-12 11:55:49 -05:00
arkon 6b61ead0b6 Disallow PackageInstaller extension installer option on MIUI
(cherry picked from commit 8c1662cfdb)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-02-12 11:55:41 -05:00
arkon d1c40b8b85 Allow disabling secure screen when incognito mode is on
(cherry picked from commit 299e52e877)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceValues.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSecurityController.kt
2022-02-12 11:53:10 -05:00
arkon 75096e9808 Don't show error toasts in MangaController for HTTP 103 responses (closes #6562)
(cherry picked from commit 95b253db09)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
2022-02-12 11:46:00 -05:00
arkon 1fdede99a0 Add shortcut to backups guide
(cherry picked from commit 067cb2452e)
2022-02-12 11:41:31 -05:00
arkon f50d23dfe6 Increase minimum required disk space to download chapters to 200MB (closes #6576)
(cherry picked from commit 45e4092335)
2022-02-12 11:41:22 -05:00
arkon 255a09abf5 Update versions plugin
(cherry picked from commit 7659a997cf)
2022-02-12 11:41:13 -05:00
arkon 0090dfcadc Filter archive files as sequence
(cherry picked from commit aa5e428222)
2022-02-12 11:41:03 -05:00
Midyan Hamdoun e9f175db5d Display correct string on FAB
(cherry picked from commit 319e4360c8)
2022-02-12 11:40:53 -05:00
arkon faaf0fbff3 Add 5% webtoon reader side padding option (closes #6511)
(cherry picked from commit f5c6e80dbb)
2022-02-12 11:40:45 -05:00
Ivan Iskandar 2025e1bc03 Unify reader error layout (#6512)
So nobody will think that the error layout is broken when they see different
layout.

(cherry picked from commit 7108993936)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
2022-02-12 11:40:39 -05:00
Ivan Iskandar 7635373446 ReaderActivity: Fix transition crash on Android 8 (#6542)
(cherry picked from commit b6553bdc34)
2022-02-12 11:39:39 -05:00
Jobobby04 0464ec0b59 Fix reader dialog colors 2022-02-12 11:31:32 -05:00
CrepeTF a0af459cfc Improve migration bottom sheet design 2022-02-10 20:41:23 -05:00
Jobobby04 b98dc6e1a5 Improve/Fix E-H redirect, add history handling, fix redirect and library handling 2022-02-08 19:48:00 -05:00
Jobobby04 0680e0120f Fix https://github.com/tachiyomiorg/tachiyomi-extensions/issues/10759 for SY 2022-02-08 19:29:15 -05:00
Jobobby04 82688f96db Revert "Temporarily revert some things for stable release"
This reverts commit e6f7689149.
2022-02-01 18:06:22 -05:00
Jobobby04 2228b24e69 Update imports 2022-02-01 18:06:04 -05:00
arkon e6f7689149 Temporarily revert some things for stable release
(cherry picked from commit b88f8ae9d2)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
(cherry picked from commit 2f22f56b32)
(cherry picked from commit 2492803741)
2022-02-01 18:03:04 -05:00
Jobobby04 5466832187 Revert "Temporarily revert some things for stable release"
This reverts commit 2492803741.
2022-02-01 17:57:55 -05:00
arkon 2492803741 Temporarily revert some things for stable release
(cherry picked from commit b88f8ae9d2)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
(cherry picked from commit 2f22f56b32)
2022-02-01 17:57:40 -05:00
arkon ee583621be Avoid unnecessary transition setup in reader if not transitioning
(cherry picked from commit 408c7b2ca6)
2022-02-01 17:57:21 -05:00
Andreas db738e727f Fix app crashing when opening ReaderActivity with FAB (#6535)
(cherry picked from commit 271253fd0b)
2022-02-01 17:57:15 -05:00
Ivan Iskandar 60221f0fc0 TachiyomiAppBarLayout: Ignore inset visibility (#6533)
For resume button animation

(cherry picked from commit 5348154c42)
2022-02-01 17:57:07 -05:00
arkon 553dfefb3a Avoid trying to open links in invalid Huawei app
(cherry picked from commit e1b1f4f3fc)
2022-02-01 17:56:55 -05:00
arkon 6cb6405e3e Update preference dependencies
(cherry picked from commit 75a2110626)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
2022-02-01 17:56:42 -05:00
Jobobby04 4e018828c4 Revert "Temporarily revert some things for stable release"
This reverts commit 2f22f56b32.
2022-02-01 17:13:25 -05:00
Jobobby04 a8e3d105f1 Release v1.8.1 2022-02-01 17:13:02 -05:00
arkon 2f22f56b32 Temporarily revert some things for stable release
(cherry picked from commit b88f8ae9d2)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
2022-02-01 17:12:34 -05:00
Jobobby04 1dd010e733 Minor cleanup 2022-02-01 17:09:41 -05:00
Jobobby04 2eef81c468 Add Mangadex blocked groups and uploaders extension preference support 2022-02-01 17:09:20 -05:00
Jobobby04 ffcb5f6954 Improve collection and string utils 2022-02-01 17:07:57 -05:00
Jobobby04 375455d4a6 Fix renamed manga delete after read 2022-02-01 10:20:15 -05:00
arkon f089991e0b Use default bottom nav height
(cherry picked from commit 836a2649d3)
2022-01-31 18:44:20 -05:00
Jobobby04 26a8b9acc4 Revert "Disable update/download warnings for stable release"
This reverts commit d46879260f.
2022-01-31 18:43:51 -05:00
686 changed files with 10923 additions and 6437 deletions
+5
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
+1 -1
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 v1.8.0) - To the latest version of the app (stable is v1.8.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
+2 -2
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: "1.8.0" Example: "1.8.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 **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true
+1 -1
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 **[1.8.0](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.8.3](https://github.com/jobobby04/tachiyomisy/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
+1 -2
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
-32
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"
}
]
+21
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"
}
]
+16 -1
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:
+71 -139
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,8 +9,8 @@ plugins {
id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
} }
if (!gradle.startParameter.taskRequests.toString().contains("Debug")) { if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
apply(plugin = "com.google.gms.google-services") apply<com.google.gms.googleservices.GoogleServicesPlugin>()
// Firebase Crashlytics // Firebase Crashlytics
apply(plugin = "com.google.firebase.crashlytics") apply(plugin = "com.google.firebase.crashlytics")
} }
@@ -29,8 +25,8 @@ android {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
minSdk = AndroidConfig.minSdk minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk targetSdk = AndroidConfig.targetSdk
versionCode = 25 versionCode = 34
versionName = "1.8.0" versionName = "1.8.3"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -120,177 +116,145 @@ 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-rc01") implementation(libs.preferencektx)
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.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)
implementation(libs.markwon)
// 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)
// SY --> // SY -->
// Changelog // Changelog
implementation("com.github.gabrielemariotti.changeloglib:changelog:2.1.0") implementation(sylibs.changelog)
// Text distance (EH) // Text distance (EH)
implementation ("info.debatty:java-string-similarity:2.0.0") implementation (sylibs.simularity)
// Firebase (EH) // Firebase (EH)
implementation("com.google.firebase:firebase-analytics-ktx:20.0.2") implementation(sylibs.firebase.analytics)
implementation("com.google.firebase:firebase-crashlytics-ktx:18.2.7") implementation(sylibs.firebase.crashlytics.ktx)
// Better logging (EH) // Better logging (EH)
implementation("com.elvishew:xlog:1.11.0") implementation(sylibs.xlog)
// Debug utils (EH) // Debug utils (EH)
val debugOverlayVersion = "1.1.3" debugImplementation(sylibs.debugOverlay.standard)
debugImplementation("com.ms-square:debugoverlay:$debugOverlayVersion") "releaseTestImplementation"(sylibs.debugOverlay.noop)
"releaseTestImplementation"("com.ms-square:debugoverlay-no-op:$debugOverlayVersion") releaseImplementation(sylibs.debugOverlay.noop)
releaseImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion") testImplementation(sylibs.debugOverlay.noop)
testImplementation("com.ms-square:debugoverlay-no-op:$debugOverlayVersion")
// RatingBar (SY) // RatingBar (SY)
implementation("me.zhanghai.android.materialratingbar:library:1.4.0") implementation(sylibs.ratingbar)
} }
tasks { tasks {
@@ -321,40 +285,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()
}
@@ -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="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<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>
-5
View File
@@ -27,7 +27,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"
@@ -187,10 +186,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" />
+65 -15
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
@@ -10,6 +11,7 @@ import android.content.IntentFilter
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
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
@@ -22,6 +24,7 @@ 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 com.elvishew.xlog.LogConfiguration import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel import com.elvishew.xlog.LogLevel
@@ -35,16 +38,17 @@ import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule import com.ms_square.debugoverlay.modules.FpsModule
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
@@ -77,6 +81,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()
// if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) // if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
@@ -120,7 +125,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)
} }
@@ -139,7 +144,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)
@@ -150,17 +155,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())
@@ -179,6 +187,27 @@ 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 setupNotificationChannels() { protected open fun setupNotificationChannels() {
try { try {
Notifications.createChannels(this) Notifications.createChannels(this)
@@ -208,7 +237,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
val logFolder = File( val logFolder = File(
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.getExternalStorageDirectory().absolutePath + File.separator +
getString(R.string.app_name), getString(R.string.app_name),
"logs" "logs",
) )
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
@@ -219,7 +248,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
override fun generateFileName(logLevel: Int, timestamp: Long): String { override fun generateFileName(logLevel: Int, timestamp: Long): String {
return super.generateFileName( return super.generateFileName(
logLevel, logLevel,
timestamp timestamp,
) + "-${BuildConfig.BUILD_TYPE}.log" ) + "-${BuildConfig.BUILD_TYPE}.log"
} }
} }
@@ -237,7 +266,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
XLog.init( XLog.init(
logConfig, logConfig,
*printers.toTypedArray() *printers.toTypedArray(),
) )
xLogD("Application booting...") xLogD("Application booting...")
@@ -252,7 +281,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
Device name: ${Build.DEVICE} Device name: ${Build.DEVICE}
Device model: ${Build.MODEL} Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT} Device product name: ${Build.PRODUCT}
""".trimIndent() """.trimIndent(),
) )
} }
@@ -296,3 +325,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 }
}
}
}
@@ -8,6 +8,7 @@ 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.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.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
@@ -48,6 +49,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DelayedTrackingStore(app) } addSingletonFactory { DelayedTrackingStore(app) }
addSingletonFactory { ImageSaver(app) }
// SY --> // SY -->
addSingletonFactory { CustomMangaManager(app) } addSingletonFactory { CustomMangaManager(app) }
@@ -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
@@ -244,7 +246,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)
} }
} }
@@ -28,7 +28,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
protected val customMangaManager: CustomMangaManager by injectLazy() protected val customMangaManager: CustomMangaManager by injectLazy()
// SY <-- // SY <--
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
/** /**
* Returns manga * Returns manga
@@ -122,7 +122,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)
} }
@@ -11,4 +11,22 @@ 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
// SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_READ_MANGA = 0x20
internal const val BACKUP_READ_MANGA_MASK = 0x20
internal const val BACKUP_ALL = 0x3F
// SY <--
} }
@@ -1,121 +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
// SY -->
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_READ_MANGA = 0x20
internal const val BACKUP_READ_MANGA_MASK = 0x20
internal const val BACKUP_ALL = 0x3F
// SY <--
/**
* 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
}
}
@@ -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
@@ -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) {
@@ -3,19 +3,20 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context 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.R
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_CUSTOM_INFO import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO_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_READ_MANGA import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_READ_MANGA_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA_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
@@ -38,18 +39,17 @@ import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.executeOnIO import exh.util.executeOnIO
import kotlinx.serialization.decodeFromString import exh.util.nullIfBlank
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority 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) {
@@ -60,9 +60,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
@@ -71,21 +71,21 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
getReadManga() getReadManga()
} else { } else {
emptyList() emptyList()
} + getMergedManga() /* SY <-- */ } + getMergedManga() // SY <--
backup = Backup( backup = Backup(
backupManga(databaseManga, flags), backupManga(databaseManga, flags),
backupCategories(), backupCategories(),
emptyList(), emptyList(),
backupExtensionInfo(databaseManga), backupExtensionInfo(databaseManga),
backupSavedSearches() backupSavedSearches(),
) )
} }
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")
@@ -107,8 +107,19 @@ 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) } if (byteArray.isEmpty()) {
throw IllegalStateException(context.getString(R.string.empty_backup_error))
}
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
@@ -156,14 +167,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @return list of [BackupSavedSearch] to be backed up * @return list of [BackupSavedSearch] to be backed up
*/ */
private fun backupSavedSearches(): List<BackupSavedSearch> { private fun backupSavedSearches(): List<BackupSavedSearch> {
return preferences.savedSearches().get().mapNotNull { return databaseHelper.getSavedSearches().executeAsBlocking().map {
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
val content = Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
BackupSavedSearch( BackupSavedSearch(
content.name, it.name,
content.query, it.query.orEmpty(),
content.filters.toString(), it.filtersJson ?: "[]",
sourceId it.source,
) )
} }
} }
@@ -423,34 +432,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// SY --> // SY -->
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) { internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = preferences.savedSearches().get().mapNotNull { val currentSavedSearches = databaseHelper.getSavedSearches()
val sourceId = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null .executeAsBlocking()
val content = try {
Json.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
} catch (e: Exception) {
return@mapNotNull null
}
BackupSavedSearch(
content.name,
content.query,
content.filters.toString(),
sourceId
)
}
val newSavedSearches = backupSavedSearches.filter { backupSavedSearch -> val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.map { }.map {
"${it.source}:" + Json.encodeToString( SavedSearch(
JsonSavedSearch( id = null,
it.name, it.source,
it.query, it.name,
Json.decodeFromString(it.filterList) it.query.nullIfBlank(),
) filtersJson = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" },
) )
}.toSet() }.ifEmpty { null }
preferences.savedSearches().set(newSavedSearches + preferences.savedSearches().get()) if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
}
} }
/** /**
@@ -11,5 +11,5 @@ data class Backup(
@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(),
// SY specific values // SY specific values
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList() @ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
) )
@@ -13,7 +13,7 @@ class BackupCategory(
// 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 flags: Int = 0, @ProtoNumber(100) var flags: Int = 0,
// SY specific values // SY specific values
@ProtoNumber(600) var mangaOrder: List<Long> = emptyList() @ProtoNumber(600) var mangaOrder: List<Long> = emptyList(),
) { ) {
fun getCategoryImpl(): CategoryImpl { fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply { return CategoryImpl().apply {
@@ -30,7 +30,7 @@ class BackupCategory(
name = category.name, name = category.name,
order = category.order, order = category.order,
flags = category.flags, flags = category.flags,
mangaOrder = category.mangaOrder mangaOrder = category.mangaOrder,
) )
} }
} }
@@ -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,
) )
} }
} }
@@ -11,13 +11,13 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BackupFlatMetadata( data class BackupFlatMetadata(
@ProtoNumber(1) var searchMetadata: BackupSearchMetadata, @ProtoNumber(1) var searchMetadata: BackupSearchMetadata,
@ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(), @ProtoNumber(2) var searchTags: List<BackupSearchTag> = emptyList(),
@ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList() @ProtoNumber(3) var searchTitles: List<BackupSearchTitle> = emptyList(),
) { ) {
fun getFlatMetadata(mangaId: Long): FlatMetadata { fun getFlatMetadata(mangaId: Long): FlatMetadata {
return FlatMetadata( return FlatMetadata(
metadata = searchMetadata.getSearchMetadata(mangaId), metadata = searchMetadata.getSearchMetadata(mangaId),
tags = searchTags.map { it.getSearchTag(mangaId) }, tags = searchTags.map { it.getSearchTag(mangaId) },
titles = searchTitles.map { it.getSearchTitle(mangaId) } titles = searchTitles.map { it.getSearchTitle(mangaId) },
) )
} }
@@ -26,7 +26,7 @@ data class BackupFlatMetadata(
return BackupFlatMetadata( return BackupFlatMetadata(
searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata), searchMetadata = BackupSearchMetadata.copyFrom(flatMetadata.metadata),
searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) }, searchTags = flatMetadata.tags.map { BackupSearchTag.copyFrom(it) },
searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) } searchTitles = flatMetadata.titles.map { BackupSearchTitle.copyFrom(it) },
) )
} }
} }
@@ -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,
) )
@@ -95,7 +95,7 @@ data class BackupManga(
artist = customArtist, artist = customArtist,
description = customDescription, description = customDescription,
genre = customGenre, genre = customGenre,
status = customStatus.takeUnless { it == 0 } status = customStatus.takeUnless { it == 0 },
) )
} }
return null return null
@@ -127,7 +127,7 @@ data class BackupManga(
viewer = manga.readingModeType, viewer = manga.readingModeType,
viewer_flags = manga.viewer_flags, viewer_flags = manga.viewer_flags,
chapterFlags = manga.chapter_flags, chapterFlags = manga.chapter_flags,
filtered_scanlators = manga.filtered_scanlators filtered_scanlators = manga.filtered_scanlators,
// SY --> // SY -->
).also { backupManga -> ).also { backupManga ->
customMangaManager?.getManga(manga)?.let { customMangaManager?.getManga(manga)?.let {
@@ -16,7 +16,7 @@ data class BackupMergedMangaReference(
@ProtoNumber(5) var downloadChapters: Boolean, @ProtoNumber(5) var downloadChapters: Boolean,
@ProtoNumber(6) var mergeUrl: String, @ProtoNumber(6) var mergeUrl: String,
@ProtoNumber(7) var mangaUrl: String, @ProtoNumber(7) var mangaUrl: String,
@ProtoNumber(8) var mangaSourceId: Long @ProtoNumber(8) var mangaSourceId: Long,
) { ) {
fun getMergedMangaReference(): MergedMangaReference { fun getMergedMangaReference(): MergedMangaReference {
return MergedMangaReference( return MergedMangaReference(
@@ -30,7 +30,7 @@ data class BackupMergedMangaReference(
mangaSourceId = mangaSourceId, mangaSourceId = mangaSourceId,
mergeId = null, mergeId = null,
mangaId = null, mangaId = null,
id = null id = null,
) )
} }
@@ -44,7 +44,7 @@ data class BackupMergedMangaReference(
downloadChapters = mergedMangaReference.downloadChapters, downloadChapters = mergedMangaReference.downloadChapters,
mergeUrl = mergedMangaReference.mergeUrl, mergeUrl = mergedMangaReference.mergeUrl,
mangaUrl = mergedMangaReference.mangaUrl, mangaUrl = mergedMangaReference.mangaUrl,
mangaSourceId = mergedMangaReference.mangaSourceId mangaSourceId = mergedMangaReference.mangaSourceId,
) )
} }
} }
@@ -11,5 +11,5 @@ data class BackupSavedSearch(
@ProtoNumber(1) val name: String, @ProtoNumber(1) val name: String,
@ProtoNumber(2) val query: String = "", @ProtoNumber(2) val query: String = "",
@ProtoNumber(3) val filterList: String = "", @ProtoNumber(3) val filterList: String = "",
@ProtoNumber(4) val source: Long = 0 @ProtoNumber(4) val source: Long = 0,
) )
@@ -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,
) )
} }
} }
@@ -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,
) )
} }
} }
@@ -9,7 +9,7 @@ data class BackupSearchMetadata(
@ProtoNumber(1) var uploader: String? = null, @ProtoNumber(1) var uploader: String? = null,
@ProtoNumber(2) var extra: String, @ProtoNumber(2) var extra: String,
@ProtoNumber(3) var indexedExtra: String? = null, @ProtoNumber(3) var indexedExtra: String? = null,
@ProtoNumber(4) var extraVersion: Int @ProtoNumber(4) var extraVersion: Int,
) { ) {
fun getSearchMetadata(mangaId: Long): SearchMetadata { fun getSearchMetadata(mangaId: Long): SearchMetadata {
return SearchMetadata( return SearchMetadata(
@@ -17,7 +17,7 @@ data class BackupSearchMetadata(
uploader = uploader, uploader = uploader,
extra = extra, extra = extra,
indexedExtra = indexedExtra, indexedExtra = indexedExtra,
extraVersion = extraVersion extraVersion = extraVersion,
) )
} }
@@ -27,7 +27,7 @@ data class BackupSearchMetadata(
uploader = searchMetadata.uploader, uploader = searchMetadata.uploader,
extra = searchMetadata.extra, extra = searchMetadata.extra,
indexedExtra = searchMetadata.indexedExtra, indexedExtra = searchMetadata.indexedExtra,
extraVersion = searchMetadata.extraVersion extraVersion = searchMetadata.extraVersion,
) )
} }
} }
@@ -8,7 +8,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
data class BackupSearchTag( data class BackupSearchTag(
@ProtoNumber(1) var namespace: String? = null, @ProtoNumber(1) var namespace: String? = null,
@ProtoNumber(2) var name: String, @ProtoNumber(2) var name: String,
@ProtoNumber(3) var type: Int @ProtoNumber(3) var type: Int,
) { ) {
fun getSearchTag(mangaId: Long): SearchTag { fun getSearchTag(mangaId: Long): SearchTag {
return SearchTag( return SearchTag(
@@ -16,7 +16,7 @@ data class BackupSearchTag(
mangaId = mangaId, mangaId = mangaId,
namespace = namespace, namespace = namespace,
name = name, name = name,
type = type type = type,
) )
} }
@@ -25,7 +25,7 @@ data class BackupSearchTag(
return BackupSearchTag( return BackupSearchTag(
namespace = searchTag.namespace, namespace = searchTag.namespace,
name = searchTag.name, name = searchTag.name,
type = searchTag.type type = searchTag.type,
) )
} }
} }
@@ -7,14 +7,14 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable @Serializable
data class BackupSearchTitle( data class BackupSearchTitle(
@ProtoNumber(1) var title: String, @ProtoNumber(1) var title: String,
@ProtoNumber(2) var type: Int @ProtoNumber(2) var type: Int,
) { ) {
fun getSearchTitle(mangaId: Long): SearchTitle { fun getSearchTitle(mangaId: Long): SearchTitle {
return SearchTitle( return SearchTitle(
id = null, id = null,
mangaId = mangaId, mangaId = mangaId,
title = title, title = title,
type = type type = type,
) )
} }
@@ -22,7 +22,7 @@ data class BackupSearchTitle(
fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle { fun copyFrom(searchTitle: SearchTitle): BackupSearchTitle {
return BackupSearchTitle( return BackupSearchTitle(
title = searchTitle.title, title = searchTitle.title,
type = searchTitle.type type = searchTitle.type,
) )
} }
} }
@@ -28,11 +28,16 @@ import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.MergedSource
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.JsonSavedSearch import exh.savedsearches.models.SavedSearch
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.nullIfBlank
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual import kotlinx.serialization.modules.contextual
import kotlin.math.max import kotlin.math.max
@@ -67,9 +72,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) {
@@ -287,34 +292,26 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
internal fun restoreSavedSearches(jsonSavedSearches: String) { internal fun restoreSavedSearches(jsonSavedSearches: String) {
val backupSavedSearches = jsonSavedSearches.split("***").toSet() val backupSavedSearches = jsonSavedSearches.split("***").toSet()
val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
val newSavedSearches = backupSavedSearches.mapNotNull { val newSavedSearches = backupSavedSearches.mapNotNull {
runCatching { runCatching {
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':')) SavedSearch(
id to content id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray),
)
}.getOrNull() }.getOrNull()
}.toMutableSet() }.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.ifEmpty { null }
val currentSources = newSavedSearches.map(Pair<Long, *>::first).toSet() if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
newSavedSearches += preferences.savedSearches().get().mapNotNull {
kotlin.runCatching {
val id = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null
val content = parser.decodeFromString<JsonSavedSearch>(it.substringAfter(':'))
id to content
}.getOrNull()
} }
val otherSerialized = preferences.savedSearches().get().mapNotNull {
val sourceId = it.substringBefore(":").toLongOrNull() ?: return@mapNotNull null
if (sourceId in currentSources) return@mapNotNull null
it
}.toSet()
val newSerialized = newSavedSearches.map { (source, savedSearch) ->
"$source:" + Json.encodeToString(savedSearch)
}.toSet()
preferences.savedSearches().set(otherSerialized + newSerialized)
} }
/** /**
@@ -34,7 +34,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
@@ -143,7 +143,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)
@@ -173,7 +173,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)
@@ -195,7 +195,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)
@@ -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)
@@ -21,7 +21,7 @@ data class Backup(
// SY Specific values // SY Specific values
@SerialName("mergedmangareferences") @SerialName("mergedmangareferences")
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null, var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
var savedSearches: String? = null var savedSearches: String? = null,
) { ) {
companion object { companion object {
const val CURRENT_VERSION = 2 const val CURRENT_VERSION = 2
@@ -39,5 +39,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,
) )
@@ -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)
} },
) )
} }
@@ -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)
} }
} },
) )
} }
@@ -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,
) )
} }
} }
@@ -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)
} },
) )
} }
@@ -34,7 +34,7 @@ object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
add(value.getChapterUpdates) add(value.getChapterUpdates)
add(value.isInfoManga) add(value.isInfoManga)
add(value.downloadChapters) add(value.downloadChapters)
} },
) )
} }
@@ -52,7 +52,7 @@ object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
isInfoManga = array[6].jsonPrimitive.boolean, isInfoManga = array[6].jsonPrimitive.boolean,
downloadChapters = array[7].jsonPrimitive.boolean, downloadChapters = array[7].jsonPrimitive.boolean,
mangaId = null, mangaId = null,
mergeId = null mergeId = null,
) )
} }
} }
@@ -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)
} },
) )
} }
@@ -93,7 +93,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,
cacheSize * 1024 * 1024 cacheSize * 1024 * 1024,
) )
} }
// <-- EH // <-- EH
@@ -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 {
@@ -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
}
@@ -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"
@@ -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() }
}
}
@@ -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()
}
} }
@@ -36,13 +36,33 @@ import exh.metadata.sql.models.SearchTitle
import exh.metadata.sql.queries.SearchMetadataQueries import exh.metadata.sql.queries.SearchMetadataQueries
import exh.metadata.sql.queries.SearchTagQueries import exh.metadata.sql.queries.SearchTagQueries
import exh.metadata.sql.queries.SearchTitleQueries import exh.metadata.sql.queries.SearchTitleQueries
import exh.savedsearches.mappers.FeedSavedSearchTypeMapping
import exh.savedsearches.mappers.SavedSearchTypeMapping
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import exh.savedsearches.queries.FeedSavedSearchQueries
import exh.savedsearches.queries.SavedSearchQueries
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
/** /**
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) : open class DatabaseHelper(context: Context) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, FavoriteEntryQueries /* SY <-- */ { MangaQueries,
ChapterQueries,
TrackQueries,
CategoryQueries,
MangaCategoryQueries,
HistoryQueries,
/* SY --> */
SearchMetadataQueries,
SearchTagQueries,
SearchTitleQueries,
MergedQueries,
FavoriteEntryQueries,
SavedSearchQueries,
FeedSavedSearchQueries
/* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
@@ -63,6 +83,8 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping()) .addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
.addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping()) .addTypeMapping(FavoriteEntry::class.java, FavoriteEntryTypeMapping())
.addTypeMapping(SavedSearch::class.java, SavedSearchTypeMapping())
.addTypeMapping(FeedSavedSearch::class.java, FeedSavedSearchTypeMapping())
// SY <-- // SY <--
.build() .build()
@@ -13,6 +13,8 @@ import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
import exh.savedsearches.tables.FeedSavedSearchTable
import exh.savedsearches.tables.SavedSearchTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
@@ -25,7 +27,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = /* SY --> */ 12 /* SY <-- */ const val DATABASE_VERSION = /* SY --> */ 13 // SY <--
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -41,6 +43,8 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createTableQuery) execSQL(SearchTitleTable.createTableQuery)
execSQL(MergedTable.createTableQuery) execSQL(MergedTable.createTableQuery)
execSQL(FavoriteEntryTable.createTableQuery) execSQL(FavoriteEntryTable.createTableQuery)
execSQL(SavedSearchTable.createTableQuery)
execSQL(FeedSavedSearchTable.createTableQuery)
// SY <-- // SY <--
// DB indexes // DB indexes
@@ -57,6 +61,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createMangaIdIndexQuery) execSQL(SearchTitleTable.createMangaIdIndexQuery)
execSQL(SearchTitleTable.createTitleIndexQuery) execSQL(SearchTitleTable.createTitleIndexQuery)
execSQL(MergedTable.createIndexQuery) execSQL(MergedTable.createIndexQuery)
execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
// SY <-- // SY <--
} }
@@ -101,6 +106,11 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 12) { if (oldVersion < 12) {
db.execSQL(FavoriteEntryTable.fixTableQuery) db.execSQL(FavoriteEntryTable.fixTableQuery)
} }
if (oldVersion < 13) {
db.execSQL(SavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createTableQuery)
db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {
@@ -21,7 +21,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>() {
@@ -42,7 +42,7 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
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,
COL_MANGA_ORDER to obj.mangaOrder.joinToString("/") COL_MANGA_ORDER to obj.mangaOrder.joinToString("/"),
) )
} }
@@ -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,
) )
} }
@@ -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,
) )
} }
@@ -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,
) )
} }
@@ -34,7 +34,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>() {
@@ -70,7 +70,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
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,
COL_FILTERED_SCANLATORS to obj.filtered_scanlators COL_FILTERED_SCANLATORS to obj.filtered_scanlators,
) )
} }
@@ -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,
) )
} }
@@ -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
@@ -133,6 +133,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,
) )
} }
@@ -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()
@@ -25,7 +25,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId) .whereArgs(mangaId)
.build() .build(),
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -37,7 +37,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()
@@ -49,7 +49,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()
@@ -60,7 +60,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()
@@ -71,7 +71,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()
@@ -83,7 +83,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()
@@ -94,7 +94,7 @@ interface ChapterQueries : DbProvider {
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)") .where("${ChapterTable.COL_URL} IN (?) AND (${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0)")
.whereArgs(urls.joinToString { "\"$it\"" }) .whereArgs(urls.joinToString { "\"$it\"" })
.build() .build(),
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -5,6 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryChapterIdPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable import eu.kanade.tachiyomi.data.database.tables.HistoryTable
@@ -32,7 +33,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 +45,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByMangaId()) .query(getHistoryByMangaId())
.args(mangaId) .args(mangaId)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -55,7 +56,7 @@ interface HistoryQueries : DbProvider {
.query(getHistoryByChapterUrl()) .query(getHistoryByChapterUrl())
.args(chapterUrl) .args(chapterUrl)
.observesTables(HistoryTable.TABLE) .observesTables(HistoryTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -83,7 +84,7 @@ interface HistoryQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -93,7 +94,24 @@ 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()
// SY -->
fun updateHistoryChapterIds(history: List<History>) = db.put()
.objects(history)
.withPutResolver(HistoryChapterIdPutResolver())
.prepare()
fun deleteHistoryIds(ids: List<Long>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_ID} IN (?)")
.whereArgs(ids.joinToString())
.build(),
)
.prepare()
// SY <--
} }
@@ -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()
@@ -35,11 +35,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)
@@ -63,7 +78,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()
@@ -74,7 +89,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()
@@ -84,7 +99,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()
@@ -95,7 +110,7 @@ interface MangaQueries : DbProvider {
.withQuery( .withQuery(
Query.builder() Query.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -104,7 +119,7 @@ interface MangaQueries : DbProvider {
.withQuery( .withQuery(
RawQuery.builder() RawQuery.builder()
.query(getReadMangaNotInLibraryQuery()) .query(getReadMangaNotInLibraryQuery())
.build() .build(),
) )
.prepare() .prepare()
@@ -194,11 +209,11 @@ interface MangaQueries : DbProvider {
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN ( ${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID} SELECT ${MergedTable.COL_MANGA_ID} FROM ${MergedTable.TABLE} WHERE ${MergedTable.COL_MANGA_ID} != ${MergedTable.COL_MERGE_ID}
) )
""".trimIndent() """.trimIndent(),
) )
// SY <-- // SY <--
.whereArgs(0, *sourceIds.toTypedArray()) .whereArgs(0, *sourceIds.toTypedArray())
.build() .build(),
) )
.prepare() .prepare()
@@ -214,10 +229,10 @@ interface MangaQueries : DbProvider {
) AND ${MangaTable.COL_ID} NOT IN ( ) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0 SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
) )
""".trimIndent() """.trimIndent(),
) )
.whereArgs(0) .whereArgs(0, *sourceIds.toTypedArray())
.build() .build(),
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -226,7 +241,7 @@ interface MangaQueries : DbProvider {
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -236,7 +251,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLastReadMangaQuery()) .query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -246,7 +261,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getTotalChapterMangaQuery()) .query(getTotalChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -256,7 +271,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getLatestChapterMangaQuery()) .query(getLatestChapterMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -266,7 +281,7 @@ interface MangaQueries : DbProvider {
RawQuery.builder() RawQuery.builder()
.query(getChapterFetchDateMangaQuery()) .query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE) .observesTables(MangaTable.TABLE)
.build() .build(),
) )
.prepare() .prepare()
@@ -281,9 +296,9 @@ interface MangaQueries : DbProvider {
INNER JOIN ${SearchMetadataTable.TABLE} INNER JOIN ${SearchMetadataTable.TABLE}
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent() """.trimIndent(),
) )
.build() .build(),
) )
.prepare() .prepare()
@@ -298,9 +313,9 @@ interface MangaQueries : DbProvider {
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent() """.trimIndent(),
) )
.build() .build(),
) )
.prepare() .prepare()
@@ -315,9 +330,9 @@ interface MangaQueries : DbProvider {
ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID} ON ${MangaTable.TABLE}.${MangaTable.COL_ID} = ${SearchMetadataTable.TABLE}.${SearchMetadataTable.COL_MANGA_ID}
WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1 WHERE ${MangaTable.TABLE}.${MangaTable.COL_FAVORITE} = 1
ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID} ORDER BY ${MangaTable.TABLE}.${MangaTable.COL_ID}
""".trimIndent() """.trimIndent(),
) )
.build() .build(),
) )
.prepare() .prepare()
// SY <-- // SY <--
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
import exh.savedsearches.tables.FeedSavedSearchTable
import exh.savedsearches.tables.SavedSearchTable
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
@@ -75,23 +77,49 @@ fun getReadMangaNotInLibraryQuery() =
""" """
/** /**
* Query to get the manga from the library, with their categories and unread count. * Query to get the global feed saved searches
*/
fun getGlobalFeedSavedSearchQuery() =
"""
SELECT ${SavedSearchTable.TABLE}.*
FROM (
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1
) AS M
JOIN ${SavedSearchTable.TABLE}
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
"""
/**
* Query to get the source feed saved searches
*/
fun getSourceFeedSavedSearchQuery() =
"""
SELECT ${SavedSearchTable.TABLE}.*
FROM (
SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ?
) AS M
JOIN ${SavedSearchTable.TABLE}
ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID}
"""
/**
* 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}, COALESCE(R.read, 0) AS ${Manga.COL_READ} 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.TABLE}.${Chapter.COL_MANGA_ID}, COUNT(*) AS unread SELECT ${Chapter.TABLE}.${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.TABLE}.${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS C ) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
LEFT JOIN ( LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS read SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS readCount
FROM ${Chapter.TABLE} FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 1 WHERE ${Chapter.COL_READ} = 1
GROUP BY ${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.COL_MANGA_ID}
@@ -100,10 +128,10 @@ val libraryQuery =
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID WHERE ${Manga.COL_FAVORITE} = 1 AND ${Manga.COL_SOURCE} <> $MERGED_SOURCE_ID
GROUP BY ${Manga.TABLE}.${Manga.COL_ID} GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
UNION UNION
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}, COALESCE(R.read, 0) AS ${Manga.COL_READ} 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 ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unread SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as unreadCount
FROM ${Merged.TABLE} FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
@@ -112,7 +140,7 @@ val libraryQuery =
) AS C ) AS C
ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = C.${Merged.COL_MERGE_ID}
LEFT JOIN ( LEFT JOIN (
SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as read SELECT ${Merged.TABLE}.${Merged.COL_MERGE_ID}, COUNT(*) as readCount
FROM ${Merged.TABLE} FROM ${Merged.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ${Merged.TABLE}.${Merged.COL_MANGA_ID}
@@ -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()
} }
@@ -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,
) )
} }
@@ -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,
) )
} }
@@ -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,
) )
} }
@@ -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,
) )
} }
@@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryChapterIdPutResolver : PutResolver<History>() {
override fun performPut(db: StorIOSQLite, history: History) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history)
val contentValues = mapToContentValues(history)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(history: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_ID} = ?")
.whereArgs(history.id)
.build()
fun mapToContentValues(history: History) =
contentValuesOf(
HistoryTable.COL_CHAPTER_ID to history.chapter_id,
)
}
@@ -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,
) )
} }
@@ -16,11 +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))
// SY --> manga.readCount = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COMPUTED_COL_READ_COUNT))
manga.read = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_READ))
// SY <--
return manga return manga
} }
@@ -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,
) )
} }
@@ -28,6 +28,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,
MangaTable.COL_DATE_ADDED to manga.date_added MangaTable.COL_DATE_ADDED to manga.date_added,
) )
} }
@@ -27,6 +27,6 @@ class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
.build() .build()
fun mapToContentValues(manga: Manga) = contentValuesOf( fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators,
) )
} }
@@ -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),
) )
} }
@@ -32,7 +32,7 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
MangaTable.COL_AUTHOR to manga.originalAuthor, MangaTable.COL_AUTHOR to manga.originalAuthor,
MangaTable.COL_ARTIST to manga.originalArtist, MangaTable.COL_ARTIST to manga.originalArtist,
MangaTable.COL_DESCRIPTION to manga.originalDescription, MangaTable.COL_DESCRIPTION to manga.originalDescription,
MangaTable.COL_STATUS to manga.originalStatus MangaTable.COL_STATUS to manga.originalStatus,
) )
private fun resetToContentValues(manga: Manga) = contentValuesOf( private fun resetToContentValues(manga: Manga) = contentValuesOf(
@@ -41,7 +41,7 @@ class MangaInfoPutResolver(val reset: Boolean = false) : PutResolver<Manga>() {
MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(), MangaTable.COL_AUTHOR to manga.author?.split(splitter)?.lastOrNull(),
MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(), MangaTable.COL_ARTIST to manga.artist?.split(splitter)?.lastOrNull(),
MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(), MangaTable.COL_DESCRIPTION to manga.description?.split(splitter)?.lastOrNull(),
MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull() MangaTable.COL_STATUS to manga.status.nullIfZero()?.toString()?.split(splitter)?.lastOrNull(),
) )
companion object { companion object {
@@ -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,
) )
} }
@@ -30,6 +30,6 @@ class MangaMigrationPutResolver : PutResolver<Manga>() {
MangaTable.COL_DATE_ADDED to manga.date_added, MangaTable.COL_DATE_ADDED to manga.date_added,
MangaTable.COL_TITLE to manga.title, MangaTable.COL_TITLE to manga.title,
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags, MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags,
MangaTable.COL_VIEWER to manga.viewer_flags MangaTable.COL_VIEWER to manga.viewer_flags,
) )
} }
@@ -27,6 +27,6 @@ class MangaThumbnailPutResolver : PutResolver<Manga>() {
.build() .build()
fun mapToContentValues(manga: Manga) = contentValuesOf( fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url MangaTable.COL_THUMBNAIL_URL to manga.thumbnail_url,
) )
} }
@@ -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,
) )
} }
@@ -27,6 +27,6 @@ class MangaUrlPutResolver : PutResolver<Manga>() {
.build() .build()
fun mapToContentValues(manga: Manga) = contentValuesOf( fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_URL to manga.url MangaTable.COL_URL to manga.url,
) )
} }
@@ -39,11 +39,7 @@ object MangaTable {
const val COL_CHAPTER_FLAGS = "chapter_flags" const val COL_CHAPTER_FLAGS = "chapter_flags"
const val COL_UNREAD = "unread" // SY -->
// SY ->>
const val COL_READ = "read"
const val COL_FILTERED_SCANLATORS = "filtered_scanlators" const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
// SY <-- // SY <--
@@ -51,6 +47,11 @@ object MangaTable {
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(
@@ -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"
@@ -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(),
) { ) {
/** /**
@@ -251,7 +251,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(),
) )
/** /**
@@ -259,7 +259,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(),
) )
/** /**
@@ -267,7 +267,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(),
) )
/** /**
@@ -32,7 +32,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()
@@ -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)
@@ -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,14 +148,14 @@ 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,
) )
/** /**
* Returns a manga entry from a manga model. * Returns a manga entry from a manga model.
*/ */
private fun Manga.toEntry(): MangaEntry { private fun Manga.toEntry(): MangaEntry {
return MangaEntry(id!!, url, title, source) return MangaEntry(id!!, url, originalTitle, source)
} }
/** /**
@@ -121,7 +121,7 @@ class DownloadProvider(private val context: Context) {
fun findUnmatchedChapterDirs( fun findUnmatchedChapterDirs(
chapters: List<Chapter>, chapters: List<Chapter>,
manga: Manga, manga: Manga,
source: Source source: Source,
): List<UniFile> { ): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return mangaDir.listFiles().orEmpty().asList().filter { return mangaDir.listFiles().orEmpty().asList().filter {
@@ -164,7 +164,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
} },
) )
} }
@@ -183,7 +183,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),
) )
} }
} }
@@ -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()
@@ -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,
) { ) {
/** /**
@@ -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
@@ -59,7 +60,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()
@@ -210,7 +211,7 @@ class Downloader(
downloadChapter(download).subscribeOn(Schedulers.io()) downloadChapter(download).subscribeOn(Schedulers.io())
} }
}, },
5 5,
) )
.onBackpressureLatest() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@@ -222,7 +223,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)
} },
) )
} }
@@ -273,15 +274,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)
} }
@@ -482,7 +489,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") }
@@ -563,9 +570,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
@@ -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>()
@@ -22,7 +22,7 @@ class CustomMangaManager(val context: Context) {
val json = try { val json = try {
Json.decodeFromString<MangaList>( Json.decodeFromString<MangaList>(
editJson.bufferedReader().use { it.readText() } editJson.bufferedReader().use { it.readText() },
) )
} catch (e: Exception) { } catch (e: Exception) {
null null
@@ -67,13 +67,13 @@ class CustomMangaManager(val context: Context) {
artist, artist,
description, description,
genre?.split(", "), genre?.split(", "),
status status,
) )
} }
@Serializable @Serializable
data class MangaList( data class MangaList(
val mangas: List<MangaJson>? = null val mangas: List<MangaJson>? = null,
) )
@Serializable @Serializable
@@ -84,7 +84,7 @@ class CustomMangaManager(val context: Context) {
val artist: String? = null, val artist: String? = null,
val description: String? = null, val description: String? = null,
val genre: List<String>? = null, val genre: List<String>? = null,
val status: Int? = null val status: Int? = null,
) { ) {
fun toManga() = MangaImpl().apply { fun toManga() = MangaImpl().apply {
@@ -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)
@@ -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
@@ -86,31 +87,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(),
) )
} }
@@ -140,8 +177,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)
} },
) ),
) )
} }
} }
@@ -156,7 +193,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(getNotificationIntent()) setContentIntent(getNotificationIntent())
setAutoCancel(true) setAutoCancel(true)
} },
) )
// Per-manga notification // Per-manga notification
@@ -201,8 +238,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(
@@ -211,8 +248,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
@@ -224,8 +261,8 @@ class LibraryUpdateNotifier(private val context: Context) {
context, context,
manga, manga,
chapters, chapters,
Notifications.ID_NEW_CHAPTERS Notifications.ID_NEW_CHAPTERS,
) ),
) )
} }
} }
@@ -252,7 +289,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
@@ -304,10 +341,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"
@@ -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.PreferenceValues.GroupLibraryMode import eu.kanade.tachiyomi.data.preference.PreferenceValues.GroupLibraryMode
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
@@ -89,7 +90,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
@@ -171,7 +172,7 @@ class LibraryUpdateService(
true true
} else { } else {
instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra, target) instance?.addMangaToQueue(category?.id ?: -1, group, groupExtra)
false false
} }
} }
@@ -245,7 +246,7 @@ class LibraryUpdateService(
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT) val group = intent.getIntExtra(KEY_GROUP, LibraryGroup.BY_DEFAULT)
val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA) val groupExtra = intent.getStringExtra(KEY_GROUP_EXTRA)
addMangaToQueue(categoryId, group, groupExtra, target) addMangaToQueue(categoryId, group, groupExtra)
// 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 ->
@@ -274,13 +275,13 @@ 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, group: Int, groupExtra: String?, target: Target) { fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
val libraryManga = db.getLibraryMangas().executeAsBlocking() val libraryManga = db.getLibraryMangas().executeAsBlocking()
// SY --> // SY -->
val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get() val groupLibraryUpdateType = preferences.groupLibraryUpdateType().get()
// SY <-- // SY <--
var listToUpdate = if (categoryId != -1) { val listToUpdate = if (categoryId != -1) {
libraryManga.filter { it.category == categoryId } libraryManga.filter { it.category == categoryId }
// SY --> // SY -->
} else if ( } else if (
@@ -307,20 +308,16 @@ class LibraryUpdateService(
when (group) { when (group) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val trackingExtra = groupExtra?.toIntOrNull() ?: -1 val trackingExtra = groupExtra?.toIntOrNull() ?: -1
libraryManga.filter { val loggedServices = trackManager.services.filter { it.isLogged }
val loggedServices = trackManager.services.filter { it.isLogged } val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
val status: String = run { val statuses = loggedServices.associate {
val tracks = db.getTracks(it).executeAsBlocking() it.id to it.getStatusList().associateWith(it::getStatus)
val track = tracks.find { track -> }
loggedServices.any { it.id == track?.sync_id }
} libraryManga.filter { manga ->
val service = loggedServices.find { it.id == track?.sync_id } val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
if (track != null && service != null) { statuses[track.sync_id]?.get(track.status)
service.getStatus(track.status) } ?: "not tracked"
} else {
"not tracked"
}
}
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra (trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra
} }
} }
@@ -345,16 +342,6 @@ class LibraryUpdateService(
// SY <-- // SY <--
} }
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 }
@@ -364,9 +351,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()
} }
} }
@@ -384,10 +370,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 }
@@ -407,19 +395,33 @@ class LibraryUpdateService(
manga, manga,
) { manga -> ) { manga ->
try { try {
val (newChapters, _) = updateManga(manga, loggedServices) 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, loggedServices)
// 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) {
@@ -462,10 +464,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>) {
@@ -639,7 +644,7 @@ class LibraryUpdateService(
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.get(),
mangaToUpdate.size mangaToUpdate.size,
) )
block(manga) block(manga)
@@ -653,7 +658,7 @@ class LibraryUpdateService(
notifier.showProgressNotification( notifier.showProgressNotification(
updatingManga, updatingManga,
completed.get(), completed.get(),
mangaToUpdate.size mangaToUpdate.size,
) )
} }
@@ -686,7 +691,7 @@ class LibraryUpdateService(
dbManga = Manga.create( dbManga = Manga.create(
networkManga.url, networkManga.url,
networkManga.title, networkManga.title,
mangaDex.id mangaDex.id,
) )
dbManga.date_added = System.currentTimeMillis() dbManga.date_added = System.currentTimeMillis()
} }
@@ -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)
} }
} }

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