Compare commits

...

194 Commits

Author SHA1 Message Date
0eb8d7d081 Release 0.10.11 2021-04-19 10:52:52 -04:00
554f890ae3 Weblate translations (#4812)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Christian Elbrianno <christian.elbrianno41@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hautzii <am.03012002@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Jozef Hollý <j2.00ghz@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Manuel Tassi <manueltassi91@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: OfficialBispo <diogobispo10@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
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/eo/
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/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
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/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/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Christian Elbrianno <christian.elbrianno41@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hautzii <am.03012002@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Manuel Tassi <manueltassi91@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: OfficialBispo <diogobispo10@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Zulkifli <zulhaha1@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2021-04-19 10:33:06 -04:00
dd1743698f Theme BiometricUnlockActivity to avoid flashing light theme 2021-04-19 10:24:57 -04:00
b092e98ac9 Include extension loading errors in error logs 2021-04-19 10:18:32 -04:00
9ee6262aed Fix activity leak 2021-04-19 10:18:32 -04:00
24a2d86f41 Fix status bar icon colors in webview activity (#4903)
* Fixed status bar icon colors in webview activity

* Changed theme to Theme.Base

* Changed app theme to Theme.Base

* Update themes.xml

Co-authored-by: arkon <arkon@users.noreply.github.com>
2021-04-19 10:16:25 -04:00
b5c5c66336 [SKIP CI] Update FUNDING.yml (#4907) 2021-04-19 09:31:34 -04:00
7654feb6a8 Fix chapter read status not being migrated (fixes #4892) 2021-04-18 13:07:53 -04:00
a598ac3993 Update LeakCanary 2021-04-18 12:55:17 -04:00
cab919d74c Clean up controller viewbinding creation
Based on https://github.com/Jays2Kings/tachiyomiJ2K/blob/master/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
2021-04-18 12:54:51 -04:00
60a929b92c Fix source SearchView stuck open until query submitted (#4897)
closes #4850
2021-04-18 11:32:22 -04:00
356b7c346a Clean up ChapterCache (remove Gson, Rx usage) 2021-04-18 11:30:16 -04:00
ad57fde1c5 Themes cleanup (#4894) 2021-04-18 11:29:56 -04:00
17f7dea21b Update KotlinX dependencies 2021-04-17 19:19:08 -04:00
b40af7c3c6 Minor cleanup 2021-04-17 19:05:35 -04:00
9065362fde Move reading mode toast to default bottom position
Toasts don't block user interaction, so it's probably fine.
2021-04-17 18:52:52 -04:00
d264b03ca1 [SKIP CI] Update README banner image (#4887)
* Delete screens.png

* Add files via upload
2021-04-17 18:49:47 -04:00
ad9bad3d17 Adjust ActionToolbar positioning
Have I ever mentioned that I hate insets?
2021-04-17 13:07:25 -04:00
dfd858034f Avoid duplicate actions in update notifications 2021-04-17 12:58:14 -04:00
58ad8fa8c0 Add clipboard error string
I pulled a Jay and forgot to stage something.
2021-04-17 12:33:04 -04:00
38610d8a24 Avoid crash when users copying to clipboard fails because they have apps that are listening to their clipboards but also denied permissions
See https://commonsware.com/blog/2013/08/08/developer-psa-please-fix-your-clipboard-handling.html
2021-04-17 12:29:22 -04:00
27cec697bf Avoid rare crash in WebViewActivity 2021-04-17 12:22:58 -04:00
024f9a8c76 [SKIP CI] Add string for EOL update check message 2021-04-17 11:54:17 -04:00
f7cc36f2f0 Follow chapter sort setting for start/resume FAB (closes #1716) 2021-04-17 11:38:08 -04:00
ef5148ebb4 Double tap Updates to go to Download Queue (closes #4884) 2021-04-17 11:13:09 -04:00
6dbc0a6fd5 Use DSL for creating chapter description spanned string 2021-04-17 11:06:30 -04:00
fba3f9d501 Follow chapter sort setting when downloading next n chapters (closes #4725) 2021-04-17 10:51:38 -04:00
d9f8137362 Update issue templates 2021-04-16 23:16:46 -04:00
28416489b2 Adjust MoreController bottom padding for navbar 2021-04-16 23:10:38 -04:00
54a23ddd1f Long press reader settings icon to open color filter tab
Partially addresses #4867
2021-04-16 23:06:24 -04:00
3287ca9cf2 Add checkmark beside selected popup menu item
Based on what's in J2K. Also renamed to MaterialSpinnerView to match what's there.

Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
2021-04-16 22:39:19 -04:00
a59e134862 Case insensitive source directory search 2021-04-16 22:27:00 -04:00
1f8c5b0120 Adjust ActionToolbar positioning 2021-04-16 22:26:41 -04:00
c7f839ea4a Minor cleanup 2021-04-15 10:09:16 -04:00
d981245723 Remove toolbar snapping 2021-04-15 10:05:47 -04:00
1f729f1cb3 Add navigation bar scrim (#4845)
* Revert "Add navigation bar scrim (closes #4836)"

This reverts commit 2a69d1b0

* Add navigation bar scrim
2021-04-15 09:55:39 -04:00
b4577d6676 Avoid crash when unknown reading mode is used 2021-04-14 18:03:48 -04:00
544adb9940 Handle reader toolbar subtitle getting cut off when text is too big (closes #4843) 2021-04-14 08:59:23 -04:00
1875c4a752 Include chapter fetch date when migrating
Based on ee4f3e6586

Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
2021-04-14 08:57:00 -04:00
5f0493f1e5 Fix webtoon mode not calling OnPageSelected in some cases (in upstream too)
This fix isn't 100% tested, but like 80%.

@arkon if you're reading this, this issue is happening up stream too. I can make a issue for it in the repo but haven't checked if it happens there:

Steps:
Get Cubari source, search "cubari:imgur/3iOqiIy" change to continuous vertical, crop borders. Then back out and open the chapter again. onPageSelected isn't called because recycler position is -1. Regardless of the 4 pages you should be on

also fyi just a slight scroll fixes this issue but still

(cherry picked from commit 88fd6e5c9897d4a528f93dd02cfa2a4c644a799d)
2021-04-14 08:49:48 -04:00
c749e50bec Edge-to-edge in licenses activity 2021-04-13 22:48:54 -04:00
a4e5e3ece5 Use accent color for edge effect 2021-04-13 22:48:39 -04:00
2a69d1b051 Add navigation bar scrim (closes #4836) 2021-04-13 18:23:06 -04:00
126e1e2d9d Allow weaker unlock methods in Android 6 - 10 (fixes #4833) 2021-04-13 15:02:57 -04:00
0586e1d3ad Include debug info in dumped crash logs 2021-04-13 09:06:41 -04:00
07cb1c237e Allow dismissing download progress notification when paused (closes #4832) 2021-04-13 08:53:46 -04:00
f4f1efe5fa Disallow forced dark mode, such as MIUI's 2021-04-13 08:51:08 -04:00
37fdf4d434 Fix toolbar elevation in History and Updates 2021-04-12 18:43:22 -04:00
99b46096a4 Fully expand source filter sheet on show (closes #4455) 2021-04-12 17:30:44 -04:00
12e90ae35e Use same non-sticky heading style as Browse for Updates/History (closes #4822) 2021-04-12 17:11:47 -04:00
023311a874 Start download when tapping update notification (closes #4825) 2021-04-12 13:43:46 -04:00
155a4dd463 Fix ActionToolbar bottom offset 2021-04-12 12:42:07 -04:00
15bed1ac4c Offset appbar using margin instead (maybe fixes #4819) 2021-04-12 09:01:11 -04:00
27f55f8098 Fix LibraryUpdateServiceTest so ./gradlew ... doesn't crash (#4821) 2021-04-12 08:35:00 -04:00
00598879e2 Insets fix for migration manga list 2021-04-11 22:57:54 -04:00
df274a0a78 Always create releases as draft 2021-04-11 18:40:54 -04:00
0dc4862d79 Revert case insensitive source folder check 2021-04-11 18:19:41 -04:00
a3f1b72126 Lint fixes/ignore some errors 2021-04-11 18:16:15 -04:00
5ff10799e4 Release 0.10.10 2021-04-11 17:59:07 -04:00
a82e5f5452 Make library update/backup error log action clearer for non-technical users 2021-04-11 16:19:56 -04:00
e10cb0e632 Add locales: jv, lt, ne 2021-04-11 16:03:03 -04:00
c7e07a6df0 Update DoH translations 2021-04-11 16:02:02 -04:00
2e0c778090 Weblate translations (#4647)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/jv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@gmail.com>
2021-04-11 15:56:32 -04:00
592050c668 Actually ignore the case... 2021-04-11 14:23:24 -04:00
02c9191525 Make source download folder name case-insensitive
Fixes issues from things like "Mangasee" being renamed to "MangaSee"
2021-04-11 14:03:23 -04:00
d421401626 Log "Invalid download location" issues to error log 2021-04-11 14:00:45 -04:00
b2d4e5ab84 Add Google DoH provider 2021-04-11 13:10:03 -04:00
84e023607c BrowseSourceController: Fix navigation bar insets not properly applied (#4810) 2021-04-11 10:49:23 -04:00
f145fd0dec Move deletion actions to the IO thread (#4808) 2021-04-11 10:49:13 -04:00
42a9f911d8 Update some dependencies; downgrade core-ktx
Fixes ActionMode being underneath statusbar
2021-04-10 22:29:52 -04:00
9567d55312 Revert manga title folder for saved pages (closes #4803)
People also didn't like it making their galleries more complicate to navigate.
2021-04-10 14:33:14 -04:00
531cd99247 Update to Gradle 7 2021-04-10 09:48:30 -04:00
f3660d88dd Draw edge-to-edge (#4802) 2021-04-10 09:38:55 -04:00
3accb9a08b [SKIP CI] Add lock workflow 2021-04-10 09:36:01 -04:00
63ce7371bb Update some internal dependencies
They no longer rely on jcenter
2021-04-09 22:44:59 -04:00
01c3498dbf Search in library include manga description (#4787)
Co-Authored-By: jobobby04 <jobobby04@gmail.com>

Co-authored-by: jobobby04 <jobobby04@gmail.com>
2021-04-08 17:56:33 -04:00
b3471234ad Update NDK, more KTX usage (#4792)
* Update NDK

* Utilize more KTX extensions
2021-04-08 17:56:01 -04:00
b2d697131c Add clarification for category exclusion (closes #4777) 2021-04-06 23:29:46 -04:00
ef49fc91d8 Minor cleanup 2021-04-06 23:21:21 -04:00
6222b47a4f Flip crop borders and orientation toggles 2021-04-06 22:31:36 -04:00
f58e3c390a Update Kotlin and Kotlinter 2021-04-06 22:29:56 -04:00
7504621a24 Make reader spinner colors a bit more consistent 2021-04-06 22:29:39 -04:00
88e49a9b8b Align filter spinners (closes #2995) 2021-04-06 11:21:39 -04:00
5b23f29d06 Revert using fetch date for updates list
Spamming the list post-migration is currently a more common usecase than sources without chapter dates. We'll need to figure out a better way of handling both scenarios.
2021-04-04 18:11:11 -04:00
c1bdebee78 Fix global update category exclusion 2021-04-04 18:09:07 -04:00
ddd4cc10ff add sort by date fetched in library (#4773)
* add sort by date fetched in library

* chapter fetch date to 8
2021-04-04 17:18:28 -04:00
0ca62a4acc Allow excluding categories from auto-download
Closes #1412

Supersedes #4121
2021-04-04 17:15:06 -04:00
4f1275ac01 Allow excluding categories from library update
Closes #3467, #4661, #1839

Supersedes #4474
2021-04-04 16:48:39 -04:00
b2fee7035f Use Material Dialogs for auto-download categories preference
To allow for negative selections in the future.
2021-04-03 16:13:12 -04:00
e15d7cb548 Use Material Dialogs for global update categories preference
To allow for negative selections in the future.
2021-04-03 16:07:42 -04:00
3257cbe21f Fix label overflow for reader spinner preferences 2021-04-03 15:46:07 -04:00
1237af1ff3 Move BiometricUtil to correct package 2021-04-03 11:38:01 -04:00
68600b337e Allow weaker unlock methods (closes #4265) 2021-04-03 11:35:33 -04:00
dac2072eaa Use app name for page download folder and use manga title subfolders (closes #4684) 2021-04-03 10:40:35 -04:00
1b921f9845 Make extension load error logs less verbose 2021-04-03 10:27:40 -04:00
a3992d9fbe Minor cleanup 2021-04-03 10:12:31 -04:00
efd2a0cb7b Replace reading mode snackbar with toast (#4752) 2021-04-03 10:07:49 -04:00
fba428257b Remove weird cropping from icon when showing missing chapter warning (#4769) 2021-04-03 10:00:23 -04:00
ff36901007 Don't repeatedly vibrate/make sounds on download progress 2021-04-01 12:18:54 -04:00
940d8389b5 Add QuadStateCheckBox view 2021-03-31 23:03:42 -04:00
f7a6cbe5e2 Revert "Drop support for Android 5.x"
This reverts commit 443024cebb. Guess I'll do this a bit later so scb can get another major update first.

April Fools or whatever.
2021-03-31 22:20:59 -04:00
7aa379a857 Better handle webtoon SSIV crop border change 2021-03-31 22:20:17 -04:00
443024cebb Drop support for Android 5.x
It's 5-6 years old, and only accounts for 2% of users in the Firebase analytics.
2021-03-30 23:15:17 -04:00
1657f04d55 Add tooltips for previous/next chapter buttons
Based on d0738f5b00
2021-03-30 23:11:36 -04:00
407e798fdb Recreate webtoon SSIV when crop borders setting changes (fixes #4734) 2021-03-30 18:47:44 -04:00
4054f2a6a0 Add icon for crop border shortcut off state 2021-03-30 18:27:09 -04:00
468cdf603c Allow translating DNS over HTTPS (closes #4747) 2021-03-30 17:54:31 -04:00
988ec6a224 Fix nav overlay always showing on start (fixes #4736) 2021-03-29 16:54:32 -04:00
bdbdf211e2 Remove insert page when dual page split get turned off (#4739) 2021-03-29 16:54:20 -04:00
0437703cbf Fix binding of intarray preferences (maybe fixes #4728) 2021-03-28 17:06:56 -04:00
71aa592111 Use regular crop icon 2021-03-28 17:06:07 -04:00
d501c02f8b Add crop borders shortcut 2021-03-28 16:25:53 -04:00
9daf0e78b8 Remove ALPHA from dual page split label 2021-03-28 16:15:11 -04:00
dfa07a5f35 Clean up SpinnerPreference a bit 2021-03-28 16:13:59 -04:00
437c995d12 Show nav overlay on invert tap change
Based on db4eca90e9
2021-03-28 16:13:34 -04:00
cc6ae9d1a8 Fix Some Bangumi Track Bug (#4726) 2021-03-28 11:36:29 -04:00
c58e4f4dee Prevent manga title from jumping (fixes #4709) 2021-03-28 11:20:19 -04:00
c87b0e77de Show number of manga per source in migrate menu (#4703) 2021-03-28 11:11:19 -04:00
355d5af8ae Dismiss action toolbar after download action in updates (closes #4729) 2021-03-28 10:59:35 -04:00
3d99a8ebdb Fix fullscreen not applying on opening reader (fixes #4723) 2021-03-28 10:48:41 -04:00
c4b975b777 Cleanup reader spinner layouts 2021-03-27 17:59:52 -04:00
2911fe7a1a Add onPause\onResume persistence to searchView. Fixes issue #3627 (#4494)
* Add onPause\onResume persistence to searchView. Fixes issue #3627

* New controller subclass with built-in SearchView support

* Implement new SearchableNucleusController in SourceController

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

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

* Continue conversion to SearchableNucleusController

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

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

* refactoring & enforce @StringRes for queryHint
2021-03-27 16:38:41 -04:00
14c114756d Clean up reader sheet spinner preferences
Based on fe2543b9d5

Co-Authored-By: Jays2Kings
2021-03-27 16:28:49 -04:00
e7a8107279 Reduce height of sheet when on color filter tab 2021-03-27 15:15:31 -04:00
bff73b1b40 Add tooltips to bottom reader menu items 2021-03-27 10:53:31 -04:00
c255f57d95 Reorganize reader sheet contents a bit 2021-03-27 10:53:19 -04:00
64c47bbaed Split general and reading mode sheet settings 2021-03-26 22:31:21 -04:00
e0b7698d40 Merge reader settings and color filter sheets
Heavily influenced by fe2543b9d5 (diff-8f47d7b7b53769ac18c28fe9978140c6bef44709879567acab2c6ef3270cd3a8)
2021-03-25 23:10:22 -04:00
a01792ac9a Maybe make opening file picker for choosing backup file more reliable 2021-03-25 13:56:39 -04:00
3ba078f64c Use more common MIME type for protobuf 2021-03-25 13:46:53 -04:00
a16240f123 Show unread entries first when sorting by unread (closes #4711)
Based on b212f8233e
2021-03-24 09:27:00 -04:00
e5a120e778 Update plugins 2021-03-24 09:26:27 -04:00
2ba60e9114 Added Start/Finished Date Support to AniList
Based on 1e3de8a67f

Co-Authored-By: Jays2Kings
2021-03-22 22:38:14 -04:00
472ce5a5e4 Fix migration due to variable shadowing (#4689) 2021-03-21 19:47:17 -04:00
99ba84c810 Handle null Anilist start dates (fixes #4685) 2021-03-20 16:36:31 -04:00
78285bdf37 Minor code cleanup 2021-03-20 15:58:54 -04:00
5a7f2684b3 Add navigation layout overlay (#4683)
* Add navigation layout overlay

* Minor clean up 

Destroy animator when done not on start
Move and change pref title 
Add summary
2021-03-20 15:36:01 -04:00
d912a42249 Fix chapters list getting updated from wrong thread (fixes #4505) 2021-03-20 15:35:02 -04:00
6d8c4fb8b1 Fix Bangumi search null image errors 2021-03-20 10:22:11 -04:00
a63cecbfcb Make tapping available extension row prompt install 2021-03-20 10:10:58 -04:00
4a5bceb4e4 Fix offline restore ignoring manga from not installed sources (fixes #4679) 2021-03-20 10:03:13 -04:00
86541445b7 Update AGP 2021-03-20 09:54:29 -04:00
4e826aa8e7 Use newer action for build workflow 2021-03-20 09:53:17 -04:00
b6e6f490e9 Implement migration for source search (#4657) 2021-03-19 23:40:09 -04:00
2145e878a4 Limit query for recent chapters to 500 (#4678) 2021-03-19 23:39:36 -04:00
355f6db255 [SKIP CI] Update README.md (#4667)
Fix link to Code of Conduct.
2021-03-18 16:52:48 -04:00
bc7632bf02 [SKIP CI] Add Code of Conduct (#4665)
* Add Code of Conduct

* Update badge section

* Add Code of Conduct link to README

* Change to relative links
2021-03-18 09:28:08 -04:00
609d8c9685 Add icons for reading mode toggle 2021-03-14 17:13:20 -04:00
2f08515455 Less janky enum iteration 2021-03-14 17:03:43 -04:00
7f450e185d Use fetch date instead of upload date when querying recent chapters (#4645) 2021-03-14 16:38:21 -04:00
747879b4ec Remove __cfduid cookie check
As per email:

Cloudflare is deprecating the __cfduid cookie and the cf-request-id headers. The __cfduid cookie will be removed on 10 May 2021 and the cf-request-id headers will be removed on 1 July. We expect that most customers will not have to take action as a result of this removal. [...] Starting on 10 May 2021, we will stop adding a “Set-Cookie” header on all HTTP responses. The last __cfduid cookies will expire 30 days after that.
2021-03-14 16:24:14 -04:00
4193870fa6 Library update freq: add 4 & 8 hours (#4557) 2021-03-14 16:22:10 -04:00
cdc5de3f1b Flip order of previous chapter reader transition text (closes #4608) 2021-03-14 16:18:52 -04:00
bc34d4fa88 Round snackbar corners 2021-03-14 16:15:25 -04:00
6fd4af8736 Adjust reader navigation button ripples 2021-03-14 16:13:18 -04:00
b5c2934270 Refactor LibraryUpdateService a bit for future changes 2021-03-14 16:08:00 -04:00
94f5117941 Remove online protobuf backup restore option 2021-03-13 18:45:22 -05:00
112e233498 Use Material dialogs for preferences
Partially addresses #2907
2021-03-13 18:00:24 -05:00
18b1326f3a Tweak dialog corner radius 2021-03-13 17:18:22 -05:00
1e58b05ead Add reading mode toggle 2021-03-13 16:47:16 -05:00
938919bd9b Move reader setting related classes 2021-03-13 16:24:44 -05:00
b6b78994d8 Move clear history from advanced settings to history screen menu (closes #4613) 2021-03-13 16:09:12 -05:00
fddd8ce305 Add "my" locale 2021-03-13 16:00:13 -05:00
ccff337975 Weblate translations (#4461)
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ka/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uz/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
2021-03-13 15:58:55 -05:00
fde6b7af4f Disable sensor when using force orientation (closes #4618) 2021-03-13 15:47:43 -05:00
0657db7dcb Allow scrolling within reader color filter sheet (fixes #4612) 2021-03-13 15:20:07 -05:00
d1c2eaf6d5 Update URL for Local Manga guide (#4641) 2021-03-13 11:38:06 -05:00
91bb6b9016 Dependency updates 2021-03-12 09:06:13 -05:00
90351c6e9e Revert to core-ktx:1.5.0-beta01
Fixes bottom reader menu from being hidden behind navbar on Android 5.0.
2021-03-07 23:04:29 -05:00
dd4740e54f [SKIP CI] Automatically reopen issues when valid 2021-03-07 10:43:35 -05:00
48e7cbd76c Fix a decoder crash with RAR files 2021-03-05 18:52:58 +01:00
f51e32f39b Avoid crash during migration 2021-02-28 16:26:05 -05:00
ae42f59102 Hide subtitle in migration list of sources if no language set (i.e. uninstalled source) 2021-02-28 16:26:05 -05:00
5c8006f9b7 Use correct background for left chapter button in reader 2021-02-28 16:26:05 -05:00
aa5861d3ca AndroidX dependency updates 2021-02-28 16:26:05 -05:00
7a64bf55cb Dual page split allow to have different setting for Paged and Webtoon (#4527) 2021-02-28 16:17:37 -05:00
d4c9ab793f Fix a decoder crash 2021-02-23 16:53:57 +01:00
48d2849d97 Support CMYK and YCCK JPEGs and fix bad PNG cropping 2021-02-22 20:43:15 +01:00
776610d0e6 Let users invert dual page split (#4470)
* Let users invert dual page split

* Use Activity lifecycleScope and cleanup invert logic
2021-02-20 09:26:57 -05:00
3a790f3d66 Add Right and Left to reader settings (#4489)
* Add Right and Left to settings

* Fix whoopsie and minor tweak to how the array is fetched
2021-02-15 12:06:03 -05:00
7382042288 Add Twitter link to About section 2021-02-15 11:58:25 -05:00
33992d80bf Add orientation toggle to bottom reader menu 2021-02-13 18:50:50 -05:00
a92b0e567b Reword bookmark strings to clarify it's for a chapter, not a page 2021-02-13 17:27:40 -05:00
829a65e515 Adjust reader seekbar design
- Revert back to old prev/next chapter icons
- Make views taller for easier actions
- Use more consistent spacing
- Add ripples to prev/next chapter buttons
2021-02-13 17:00:00 -05:00
03ad48c055 [SKIP CI] Add instructions on how to get crash logs in issue templates 2021-02-13 16:07:30 -05:00
89837e4ced Initial adoption of bottom reader menus from TachiyomiSY
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
Co-authored-by: CrepeTF <CrepeTF@users.noreply.github.com>
2021-02-13 10:47:17 -05:00
ace1db21d1 Rename drawable with more consistent naming 2021-02-13 10:44:35 -05:00
8bb69c455b Allow clicking the toolbar to go to the manga
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
2021-02-13 10:26:59 -05:00
2dae706198 Avoid crash when source list is animating 2021-02-12 17:31:17 -05:00
3eda2a220a Avoid rare crashes in settings search for ListPreferences 2021-02-12 17:22:01 -05:00
61e5440b7c Avoid crash when device fails to handle opening a URL 2021-02-12 17:02:37 -05:00
2e2663bad9 Avoid crash if activity is already dead 2021-02-12 16:55:14 -05:00
f4dd150b70 [SKIP CI] Update to issue-closer-action@v2.0 2021-02-12 16:26:48 -05:00
2b35d22e25 Switch back to new image decoder for preview builds 2021-02-12 16:07:48 -05:00
267 changed files with 6348 additions and 3029 deletions

1
.github/FUNDING.yml vendored
View File

@ -1,2 +1 @@
github: inorichi
ko_fi: inorichi

View File

@ -2,9 +2,15 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- I have updated:
- To the latest version of the app (stable is v0.10.10)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
@ -24,3 +30,5 @@ I acknowledge that:
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

View File

@ -9,9 +9,15 @@ labels: "bug"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- I have updated:
- To the latest version of the app (stable is v0.10.10)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
@ -34,3 +40,5 @@ This happened instead.
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

View File

@ -9,9 +9,14 @@ labels: "feature"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.9)
- I have updated all extensions
- I have updated:
- To the latest version of the app (stable is v0.10.10)
- All extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 454 KiB

View File

@ -71,25 +71,24 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create release
- name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_MD5=$md5" >> $GITHUB_ENV
- name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.VERSION_TAG }}
release_name: Tachiyomi ${{ env.VERSION_TAG }}
name: Tachiyomi ${{ env.VERSION_TAG }}
body: |
MD5: ${{ env.APK_MD5 }}
files: |
tachiyomi-${{ env.VERSION_TAG }}.apk
draft: true
prerelease: false
- name: Upload APK to release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
asset_content_type: application/vnd.android.package-archive

View File

@ -7,31 +7,30 @@ jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose when created in wrong repo
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
- name: Autoclose when no short description provided
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*<Write short description here>*"
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
- name: Autoclose when body acknowledgement section not removed
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
- name: Autoclose when body requested information not filled out
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: |
[
{
"type": "title",
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
"message": "It was not opened in the correct repo, as the template mentioned."
},
{
"type": "title",
"regex": ".*<Write short description here>*",
"message": "The description in the title was not filled out."
},
{
"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."
}
]

19
.github/workflows/lock.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: Lock threads
on:
# Daily
schedule:
- cron: '0 * * * *'
# Manual trigger
workflow_dispatch:
inputs:
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '2'
pr-lock-inactive-days: '2'

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,6 +1,6 @@
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| ![CI](https://github.com/tachiyomiorg/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
| ![CI](https://github.com/tachiyomiorg/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android 5.0 and above.
## Features
Features include:
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Online reading from [a variety of sources](https://github.com/tachiyomiorg/tachiyomi-extensions)
* Local reading of downloaded manga
* A configurable reader with multiple viewers, reading directions and other settings.
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
@ -63,7 +63,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
<details><summary>Contributing</summary>
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
See [CONTRIBUTING.md](./CONTRIBUTING.md).
</details>
<details><summary>Code of Conduct</summary>
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
</details>
## FAQ

View File

@ -29,8 +29,8 @@ android {
minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 56
versionName = "0.10.9"
versionCode = 58
versionName = "0.10.11"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -91,6 +91,7 @@ android {
exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE")
exclude("META-INF/*.kotlin_module")
}
dependenciesInfo {
@ -119,20 +120,20 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries
implementation("androidx.annotation:annotation:1.2.0-beta01")
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-beta01")
implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.3.0-rc01"
val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@ -143,7 +144,7 @@ dependencies {
// UI library
implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
// ReactiveX
implementation("io.reactivex:rxandroid:1.2.1")
@ -152,7 +153,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client
val okhttpVersion = "4.10.0-RC1"
val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@ -162,7 +163,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON
val kotlinSerializationVersion = "1.0.1"
val kotlinSerializationVersion = "1.1.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
@ -173,7 +174,7 @@ dependencies {
// Disk
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.tachiyomiorg:unifile:17bec43")
implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
@ -186,7 +187,7 @@ dependencies {
implementation("io.requery:sqlite-android:3.33.0")
// Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
// Model View Presenter
val nucleusVersion = "3.0.0"
@ -197,14 +198,12 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library
val glideVersion = "4.11.0"
val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
// Logging
implementation("com.jakewharton.timber:timber:4.7.1")
@ -222,7 +221,8 @@ dependencies {
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1"
@ -235,7 +235,7 @@ dependencies {
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support")
}
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding
val flowbindingVersion = "0.12.0"
@ -249,7 +249,7 @@ dependencies {
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests
testImplementation("junit:junit:4.13.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19")
@ -260,12 +260,12 @@ dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.4.2"
val coroutinesVersion = "1.4.3"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
}
tasks {

View File

@ -32,7 +32,7 @@
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi.Light"
android:theme="@style/Theme.Base"
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".ui.main.MainActivity"
@ -84,7 +84,7 @@
</activity>
<activity
android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Splash" />
android:theme="@style/Theme.Base" />
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" />

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@ -127,6 +128,17 @@ object Migrations {
context.toast(R.string.myanimelist_relogin)
}
}
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
return true
}

View File

@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
}
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true)
setOnlyAlertOnce(true)
}
builder.show(Notifications.ID_BACKUP_PROGRESS)
@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
addAction(
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
context.getString(R.string.action_show_errors),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)
}

View File

@ -43,12 +43,11 @@ class BackupRestoreService : Service() {
* @param context context of application
* @param uri path of Uri
*/
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
fun start(context: Context, uri: Uri, mode: Int) {
if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_MODE, mode)
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
}
ContextCompat.startForegroundService(context, intent)
}
@ -119,13 +118,12 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
// Cancel any previous job if needed.
backupRestore?.job?.cancel()
backupRestore = when (mode) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
else -> LegacyBackupRestore(this, notifier)
}

View File

@ -26,9 +26,6 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga
import kotlinx.serialization.protobuf.ProtoBuf
import okio.buffer
import okio.gzip
@ -183,24 +180,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
/**
* Fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return Updated manga info.
*/
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
return if (online && source != null) {
val networkManga = source.getMangaDetails(manga.toMangaInfo())
manga.also {
it.copyFrom(networkManga.toSManga())
it.favorite = manga.favorite
it.initialized = true
it.id = insertManga(manga)
}
} else {
manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
fun restoreManga(manga: Manga): Manga {
return manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
}
@ -309,29 +295,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val trackToUpdate = mutableListOf<Track>()
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
// Update database
@ -340,47 +323,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
}
/**
* Restore the chapters for manga if chapters already in database
*
* @param manga manga of chapters
* @param chapters list containing chapters that get restored
* @return boolean answering if chapter fetch is not needed
*/
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false
}
chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
chapter.read = dbChapter.read
chapter.last_page_read = dbChapter.last_page_read
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
chapter.last_page_read = dbChapter.last_page_read
}
if (!chapter.bookmark && dbChapter.bookmark) {
chapter.bookmark = dbChapter.bookmark
}
}
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter ->

View File

@ -12,13 +12,12 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.Source
import okio.buffer
import okio.gzip
import okio.source
import java.util.Date
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override suspend fun performRestore(uri: Uri): Boolean {
backupManager = FullBackupManager(context)
@ -42,9 +41,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
return false
}
restoreManga(it, backup.backupCategories, online)
restoreManga(it, backup.backupCategories)
}
// TODO: optionally trigger online library + tracker update
return true
}
@ -57,23 +58,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
val history = backupManga.history
val tracks = backupManga.getTrackingImpl()
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try {
if (source != null || !online) {
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
} else {
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
} catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
}
@ -85,33 +80,30 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* Returns a manga restore observable
*
* @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json
* @param categories categories data from json
* @param history history data from json
* @param tracks tracking data from json
*/
private suspend fun restoreMangaData(
private fun restoreMangaData(
manga: Manga,
source: Source?,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction {
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) {
// Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
} else { // Manga in database
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
} else {
// Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
}
}
}
@ -123,55 +115,37 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private suspend fun restoreMangaFetch(
source: Source?,
private fun restoreMangaFetch(
manga: Manga,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
try {
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
val fetchedManga = backupManager.restoreManga(manga)
fetchedManga.id ?: return
if (online && source != null) {
updateChapters(source, fetchedManga, chapters)
} else {
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
}
backupManager.restoreChaptersForManga(fetchedManga, chapters)
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
}
private suspend fun restoreMangaNoFetch(
source: Source?,
private fun restoreMangaNoFetch(
backupManga: Manga,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
if (online && source != null) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
} else {
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
}
backupManager.restoreChaptersForManga(backupManga, chapters)
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
updateTracking(backupManga, tracks)
}
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {

View File

@ -2,17 +2,17 @@ package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Response
import okio.buffer
import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
@ -42,8 +42,7 @@ class ChapterCache(private val context: Context) {
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
}
/** Google Json class used for parsing JSON files. */
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
@ -56,7 +55,7 @@ class ChapterCache(private val context: Context) {
/**
* Returns directory of cache.
*/
val cacheDir: File
private val cacheDir: File
get() = diskCache.directory
/**
@ -71,43 +70,19 @@ class ChapterCache(private val context: Context) {
val readableSize: String
get() = Formatter.formatFileSize(context, realSize)
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
/**
* Get page list from cache.
*
* @param chapter the chapter.
* @return an observable of the list of pages.
* @return the list of pages.
*/
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
return Observable.fromCallable {
// Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
fun getPageListFromCache(chapter: Chapter): List<Page> {
// Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use {
gson.fromJson<List<Page>>(it.getString(0))
}
// Convert JSON string to list of objects. Throws an exception if snapshot is null
return diskCache.get(key).use {
json.decodeFromString(it.getString(0))
}
}
@ -119,7 +94,7 @@ class ChapterCache(private val context: Context) {
*/
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string.
val cachedValue = gson.toJson(pages)
val cachedValue = json.encodeToString(pages)
// Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
@ -199,6 +174,38 @@ class ChapterCache(private val context: Context) {
}
}
fun clear(): Int {
var deletedFiles = 0
cacheDir.listFiles()?.forEach {
if (removeFileFromCache(it.name)) {
deletedFiles++
}
}
return deletedFiles
}
/**
* Remove file from cache.
*
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
private fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal.")) {
return false
}
return try {
// Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".")
// Remove file from cache.
diskCache.remove(key)
} catch (e: Exception) {
false
}
}
private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}"
}

View File

@ -164,4 +164,14 @@ interface MangaQueries : DbProvider {
.build()
)
.prepare()
fun getChapterFetchDateManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
)
.prepare()
}

View File

@ -122,6 +122,16 @@ fun getLatestChapterMangaQuery() =
ORDER by max DESC
"""
fun getChapterFetchDateMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
/**
* Query to get the categories for a manga.
*/

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -211,16 +212,16 @@ class DownloadManager(private val context: Context) {
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters)
launchIO {
removeFromDownloadQueue(filteredChapters)
removeFromDownloadQueue(filteredChapters)
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
}
}
return filteredChapters
}
@ -249,9 +250,11 @@ class DownloadManager(private val context: Context) {
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
downloader.queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
launchIO {
downloader.queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
}
}
/**

View File

@ -27,6 +27,8 @@ internal class DownloadNotifier(private val context: Context) {
private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false)
setOnlyAlertOnce(true)
}
}
@ -81,10 +83,8 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onProgressChange(download: Download) {
with(progressNotificationBuilder) {
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
setAutoCancel(false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@ -114,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
}
setProgress(download.pages!!.size, download.downloadedImages, false)
setOngoing(true)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
@ -127,8 +128,8 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp)
setAutoCancel(false)
setProgress(0, 0, false)
setOngoing(false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions()
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)

View File

@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
return downloadsDir
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) {
Timber.w(e)
} catch (e: Throwable) {
Timber.e(e, "Invalid download directory")
throw Exception(context.getString(R.string.invalid_download_dir))
}
}
@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source))
return downloadsDir.findFile(getSourceDirName(source), true)
}
/**

View File

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

View File

@ -71,6 +71,7 @@ class LibraryUpdateService(
private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
private var updateJob: Job? = null
/**
@ -86,6 +87,8 @@ class LibraryUpdateService(
companion object {
private var instance: LibraryUpdateService? = null
/**
* Key for category to update.
*/
@ -116,17 +119,18 @@ class LibraryUpdateService(
* @return true if service newly started, false otherwise
*/
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
if (!isRunning(context)) {
return if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
ContextCompat.startForegroundService(context, intent)
return true
true
} else {
instance?.addMangaToQueue(category?.id ?: -1, target)
false
}
return false
}
/**
@ -163,6 +167,9 @@ class LibraryUpdateService(
if (wakeLock.isHeld) {
wakeLock.release()
}
if (instance == this) {
instance = null
}
super.onDestroy()
}
@ -186,23 +193,25 @@ class LibraryUpdateService(
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
instance = this
// Unsubscribe from any previous subscription if needed
updateJob?.cancel()
// Update favorite manga. Destroy service when completed or in case of an error.
val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
// Update favorite manga
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
addMangaToQueue(categoryId, target)
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
}
updateJob = ioScope.launch(handler) {
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
}
}
updateJob?.invokeOnCompletion { stopSelf(startId) }
@ -211,32 +220,41 @@ class LibraryUpdateService(
}
/**
* Returns the list of manga to be updated.
* Adds list of manga to be updated.
*
* @param intent the update intent.
* @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
fun addMangaToQueue(categoryId: Int, target: Target) {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
libraryManga.filter { it.category == categoryId }
} else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToUpdate }
} else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
libraryManga
}
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToExclude }
} else {
emptyList()
}
listToInclude.minus(listToExclude)
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
}
return listToUpdate
val selectedScheme = preferences.libraryUpdatePrioritization().get()
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
}
/**
@ -248,7 +266,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
suspend fun updateChapterList() {
val progressCount = AtomicInteger(0)
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
@ -342,7 +360,7 @@ class LibraryUpdateService(
return syncChaptersWithSource(db, chapters, manga, source)
}
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
private suspend fun updateCovers() {
var progressCount = 0
mangaToUpdate.forEach { manga ->
@ -375,7 +393,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
private suspend fun updateTrackings() {
var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged }

View File

@ -73,7 +73,7 @@ class NotificationReceiver : BroadcastReceiver() {
shareFile(
context,
intent.getParcelableExtra(EXTRA_URI),
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
ACTION_CANCEL_RESTORE -> cancelRestore(

View File

@ -23,7 +23,13 @@ object PreferenceKeys {
const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplit = "pref_dual_page_split"
const val dualPageSplitPaged = "pref_dual_page_split"
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
const val dualPageInvertPaged = "pref_dual_page_invert"
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
const val showReadingMode = "pref_show_reading_mode"
@ -73,6 +79,10 @@ object PreferenceKeys {
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"
@ -114,6 +124,7 @@ object PreferenceKeys {
const val libraryUpdateRestriction = "library_update_restriction"
const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization"
@ -154,6 +165,7 @@ object PreferenceKeys {
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val libraryDisplayMode = "pref_display_mode_library"
@ -179,7 +191,7 @@ object PreferenceKeys {
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val enableDoh = "enable_doh"
const val dohProvider = "doh_provider"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"

View File

@ -5,6 +5,8 @@ package eu.kanade.tachiyomi.data.preference
*/
object PreferenceValues {
/* ktlint-disable experimental:enum-entry-name-case */
// Keys are lowercase to match legacy string values
enum class ThemeMode {
light,
@ -25,6 +27,8 @@ object PreferenceValues {
amoled,
}
/* ktlint-enable experimental:enum-entry-name-case */
enum class DisplayMode {
COMPACT_GRID,
COMFORTABLE_GRID,

View File

@ -22,7 +22,7 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
block(get())
return asFlow()
.onEach { block(it) }
@ -36,6 +36,10 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
fun Preference<Boolean>.toggle() {
set(!get())
}
class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -89,7 +93,13 @@ class PreferencesHelper(val context: Context) {
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
@ -143,6 +153,10 @@ class PreferencesHelper(val context: Context) {
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
@ -204,6 +218,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
@ -250,6 +265,7 @@ class PreferencesHelper(val context: Context) {
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun lang() = prefs.getString(Keys.lang, "")
@ -263,7 +279,7 @@ class PreferencesHelper(val context: Context) {
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")

View File

@ -35,6 +35,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
private val api by lazy { AnilistApi(client, interceptor) }
override val supportsReadingDates: Boolean = true
private val scorePreference = preferences.anilistScoreType()
init {

View File

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri
import androidx.core.net.toUri
import com.afollestad.date.dayOfMonth
import com.afollestad.date.month
import com.afollestad.date.year
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
@ -9,6 +12,7 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
@ -30,8 +34,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val query =
"""
val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
@ -65,10 +68,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
val query =
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
val query = """
|mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) {
|id
|status
|progress
@ -82,6 +90,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date))
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
@ -92,8 +102,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val query =
"""
val query = """
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -143,8 +152,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext {
val query =
"""
val query = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -152,6 +160,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status
|scoreRaw: score(format: POINT_100)
|progress
|startedAt {
|year
|month
|day
|}
|completedAt {
|year
|month
|day
|}
|media {
|id
|title {
@ -209,8 +227,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext {
val query =
"""
val query = """
|query User {
|Viewer {
|id
@ -243,21 +260,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
(
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
?: 0
) - 1,
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga(
struct["id"]!!.jsonPrimitive.int,
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
@ -265,7 +267,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["description"]!!.jsonPrimitive.contentOrNull,
struct["type"]!!.jsonPrimitive.content,
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
date,
parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
)
}
@ -276,10 +278,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject)
)
}
private fun parseDate(struct: JsonObject, dateKey: String): Long {
return try {
val date = Calendar.getInstance()
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
)
date.timeInMillis
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
return buildJsonObject {
put("year", JsonNull)
put("month", JsonNull)
put("day", JsonNull)
}
}
val calendar = Calendar.getInstance()
calendar.timeInMillis = dateValue
return buildJsonObject {
put("year", calendar.year)
put("month", calendar.month + 1)
put("day", calendar.dayOfMonth)
}
}
companion object {
private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/"

View File

@ -44,6 +44,8 @@ data class ALUserManga(
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga
) {
@ -51,6 +53,8 @@ data class ALUserManga(
media_id = manga.media_id
status = toTrackStatus()
score = score_raw.toFloat()
started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read
library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters

View File

@ -45,8 +45,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read
track.status = statusTrack.status
track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track)
} else {
// Set default fields if it's not found in the list
@ -66,7 +68,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
track.copyPersonalFrom(remoteStatusTrack!!)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
}
return track
}

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return withIOContext {
// read status update
val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
@ -91,12 +93,24 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
val coverUrl = if (obj["images"] is JsonObject) {
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
} else {
// Sometimes JsonNull
""
}
val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int
} else {
0
}
return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
cover_url = coverUrl
summary = obj["name"]!!.jsonPrimitive.content
tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
}
}
@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.build()
// TODO: get user readed chapter here
authClient.newCall(requestUserRead)
.await()
.parseAs<Collection>()
.let {
var response = authClient.newCall(requestUserRead).await()
var responseBody = response.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
if (responseBody.contains("\"code\":400")) {
null
} else {
json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!!
track.score = it.rating!!
track
}
}
}
}

View File

@ -8,7 +8,7 @@ data class Collection(
val comment: String? = "",
val ep_status: Int? = 0,
val lasttouch: Int? = 0,
val rating: Int? = 0,
val rating: Float? = 0f,
val status: Status? = Status(),
val tag: List<String?>? = listOf(),
val user: User? = User(),

View File

@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
@ -11,52 +8,26 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
return runBlocking {
try {
val result = GithubUpdateChecker().checkForUpdate()
override fun doWork() = runBlocking {
try {
val result = GithubUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) {
val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
)
}
}
Result.success()
} catch (e: Exception) {
Result.failure()
if (result is UpdateResult.NewUpdate<*>) {
UpdaterNotifier(context).promptUpdate(result.release.downloadLink)
}
Result.success()
} catch (e: Exception) {
Result.failure()
}
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object {
private const val TAG = "UpdateChecker"

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
@ -28,6 +30,27 @@ internal class UpdaterNotifier(private val context: Context) {
context.notificationManager.notify(id, build())
}
fun promptUpdate(url: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
val pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setContentIntent(pendingIntent)
clearActions()
addAction(
android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
pendingIntent
)
}
notificationBuilder.show()
}
/**
* Call when apk download starts.
*
@ -63,19 +86,20 @@ internal class UpdaterNotifier(private val context: Context) {
* @param uri path location of apk.
*/
fun onDownloadFinished(uri: Uri) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
setContentIntent(installIntent)
clearActions()
addAction(
R.drawable.ic_system_update_alt_white_24dp,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri)
installIntent
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),
@ -96,13 +120,13 @@ internal class UpdaterNotifier(private val context: Context) {
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
clearActions()
addAction(
R.drawable.ic_refresh_24dp,
context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url)
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel),

View File

@ -163,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.")
Timber.e(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e)
}
}

View File

@ -171,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
private val COOKIE_NAMES = listOf("cf_clearance")
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
*/
const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
.build()
)
fun OkHttpClient.Builder.dohGoogle() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("8.8.4.4"),
InetAddress.getByName("8.8.8.8")
)
.build()
)

View File

@ -4,13 +4,10 @@ import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) {
@ -38,25 +35,9 @@ class NetworkHelper(context: Context) {
builder.addInterceptor(httpLoggingInterceptor)
}
if (preferences.enableDoh()) {
builder.dns(
DnsOverHttps.Builder().client(builder.build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
)
.build()
)
when (preferences.dohProvider()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
}
builder.build()

View File

@ -27,7 +27,7 @@ import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")

View File

@ -1,65 +1,39 @@
package eu.kanade.tachiyomi.ui.base.activity
import android.content.res.Configuration
import android.os.Build
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DarkThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.LightThemeVariant
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy()
private val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) ||
(
themeMode == Values.ThemeMode.system &&
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
)
}
private val lightTheme: Int by lazy {
when (preferences.themeLight().get()) {
Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
else -> {
when {
// Light status + navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 -> {
R.style.Theme_Tachiyomi_Light_Api27
}
// Light status bar + fallback gray navigation bar
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
R.style.Theme_Tachiyomi_Light_Api23
}
// Fallback gray status + navigation bar
else -> {
R.style.Theme_Tachiyomi_Light
}
}
}
}
}
private val darkTheme: Int by lazy {
when (preferences.themeDark().get()) {
Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
else -> R.style.Theme_Tachiyomi_Dark
}
}
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(
when {
isDarkMode -> darkTheme
else -> lightTheme
val isDarkMode = when (preferences.themeMode().get()) {
ThemeMode.light -> false
ThemeMode.dark -> true
ThemeMode.system -> resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
val themeId = if (isDarkMode) {
when (preferences.themeDark().get()) {
DarkThemeVariant.default -> R.style.Theme_Tachiyomi_Dark
DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_Dark_Blue
DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Dark_Amoled
}
)
} else {
when (preferences.themeLight().get()) {
LightThemeVariant.default -> R.style.Theme_Tachiyomi_Light
LightThemeVariant.blue -> R.style.Theme_Tachiyomi_Light_Blue
}
}
setTheme(themeId)
super.onCreate(savedInstanceState)
}
}

View File

@ -19,7 +19,8 @@ import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle) {
lateinit var binding: VB
protected lateinit var binding: VB
private set
lateinit var viewScope: CoroutineScope
@ -51,11 +52,12 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
return inflateView(inflater, container)
}
abstract fun createBinding(inflater: LayoutInflater): VB
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
binding = createBinding(inflater)
return binding.root
}
open fun onViewCreated(view: View) {}
@ -121,7 +123,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
*/
fun invalidateMenuOnExpand(): Boolean {
open fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
false

View File

@ -1,11 +1,14 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import eu.kanade.tachiyomi.util.system.toast
fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag)
@ -32,3 +35,12 @@ fun Controller.withFadeTransaction(): RouterTransaction {
.pushChangeHandler(OneWayFadeChangeHandler())
.popChangeHandler(OneWayFadeChangeHandler())
}
fun Controller.openInBrowser(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
} catch (e: Throwable) {
activity?.toast(e.message)
}
}

View File

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* Implementation of the NucleusController that has a built-in ViewSearch
*/
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
/**
* Used to bypass the initial searchView being set to empty string after an onResume
*/
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
/**
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
*/
protected var nonSubmittedQuery: String = ""
/**
* To be called by classes that extend this subclass in onCreateOptionsMenu
*/
protected fun createOptionsMenu(
menu: Menu,
inflater: MenuInflater,
menuId: Int,
searchItemId: Int,
@StringRes queryHint: Int? = null,
restoreCurrentQuery: Boolean = true
) {
// Inflate menu
inflater.inflate(menuId, menu)
// Initialize search option.
val searchItem = menu.findItem(searchItemId)
val searchView = searchItem.actionView as SearchView
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
searchView.maxWidth = Int.MAX_VALUE
searchView.queryTextEvents()
.onEach {
val newText = it.queryText.toString()
if (newText.isNotBlank() or acceptEmptyQuery()) {
if (it is QueryTextEvent.QuerySubmitted) {
// Abstract function for implementation
// Run it first in case the old query data is needed (like BrowseSourceController)
onSearchViewQueryTextSubmit(newText)
presenter.query = newText
nonSubmittedQuery = ""
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
nonSubmittedQuery = newText
// Abstract function for implementation
onSearchViewQueryTextChange(newText)
}
}
// clear the collapsing flag
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
}
.launchIn(viewScope)
val query = presenter.query
// Restoring a query the user had not submitted
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
searchItem.expandActionView()
searchView.setQuery(nonSubmittedQuery, false)
onSearchViewQueryTextChange(nonSubmittedQuery)
} else {
if (queryHint != null) {
searchView.queryHint = applicationContext?.getString(queryHint)
}
if (restoreCurrentQuery) {
// Restoring a query the user had submitted
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
onSearchViewQueryTextChange(query)
onSearchViewQueryTextSubmit(query)
}
}
}
// Workaround for weird behavior where searchView gets empty text change despite
// query being set already, prevents the query from being cleared
binding.root.post {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
}
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setCurrentSearchViewState(SearchViewState.FOCUSED)
} else {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
}
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onSearchMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
val localSearchView = searchItem.actionView as SearchView
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
if (localSearchView.toString().isNotBlank()) {
setCurrentSearchViewState(SearchViewState.COLLAPSING)
}
onSearchMenuItemActionCollapse(item)
return true
}
}
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Until everything is up and running don't accept empty queries
setCurrentSearchViewState(SearchViewState.LOADING)
}
private fun acceptEmptyQuery(): Boolean {
return when (currentSearchViewState) {
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
else -> false
}
}
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
// When loading ignore all requests other than loaded
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
return
}
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
// COLLAPSING -> LOADED)
if ((from != null) && (currentSearchViewState != from)) {
return
}
currentSearchViewState = to
}
/**
* Called by the SearchView since since the implementation of these can vary in subclasses
* Not abstract as they are optional
*/
protected open fun onSearchViewQueryTextChange(newText: String?) {
}
protected open fun onSearchViewQueryTextSubmit(query: String?) {
}
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
}
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
}
/**
* During the conversion to SearchableNucleusController (after which I plan to merge its code
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
* triggered
*/
override fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
false
} else {
true
}
}
}

View File

@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope
/**
* Query from the view where applicable
*/
var query: String = ""
override fun onCreate(savedState: Bundle?) {
try {
super.onCreate(savedState)

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.browse
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -50,10 +49,7 @@ class BrowseController :
return resources!!.getString(R.string.browse)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = PagerControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = PagerControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)

View File

@ -5,11 +5,11 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -56,14 +56,17 @@ open class ExtensionController :
return ExtensionPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.swipeRefresh.isRefreshing = true
binding.swipeRefresh.refreshes()
.onEach { presenter.findAvailableExtensions() }
@ -104,6 +107,8 @@ open class ExtensionController :
override fun onButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> {
if (!extension.hasUpdate) {
openDetails(extension)
@ -111,12 +116,6 @@ open class ExtensionController :
presenter.updateExtension(extension)
}
}
is Extension.Available -> {
presenter.installExtension(extension)
}
is Extension.Untrusted -> {
openTrustDialog(extension)
}
}
}
@ -147,12 +146,11 @@ open class ExtensionController :
override fun onItemClick(view: View, position: Int): Boolean {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
if (extension is Extension.Installed) {
openDetails(extension)
} else if (extension is Extension.Untrusted) {
openTrustDialog(extension)
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> openDetails(extension)
}
return false
}

View File

@ -4,12 +4,12 @@ import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
private val binding = SectionHeaderItemBinding.bind(view)
@SuppressLint("SetTextI18n")
fun bind(item: ExtensionGroupItem) {

View File

@ -19,7 +19,7 @@ data class ExtensionGroupItem(val name: String, val size: Int, val showSize: Boo
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**

View File

@ -12,9 +12,7 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.preference.Preference
import androidx.preference.PreferenceGroupAdapter
@ -23,6 +21,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -36,6 +35,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange
@ -64,10 +64,9 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
override fun createBinding(inflater: LayoutInflater): ExtensionDetailControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
return binding.root
return ExtensionDetailControllerBinding.inflate(themedInflater)
}
override fun createPresenter(): ExtensionDetailsPresenter {
@ -82,6 +81,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
val extension = presenter.extension ?: return
val context = view.context
@ -213,8 +218,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
!pkgFactory.isNullOrEmpty() -> "$URL_EXTENSION_COMMITS/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
else -> "$URL_EXTENSION_COMMITS/src/${pkgName.replace(".", "/")}"
}
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
openInBrowser(url)
}
private fun openInSettings() {

View File

@ -6,7 +6,6 @@ import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf
import androidx.preference.DialogPreference
@ -45,10 +44,9 @@ class SourcePreferencesController(bundle: Bundle? = null) :
bundleOf(SOURCE_ID to sourceId)
)
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
override fun createBinding(inflater: LayoutInflater): SourcePreferencesControllerBinding {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = SourcePreferencesControllerBinding.inflate(themedInflater)
return binding.root
return SourcePreferencesControllerBinding.inflate(themedInflater)
}
override fun createPresenter(): SourcePreferencesPresenter {

View File

@ -3,9 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
@ -44,14 +44,17 @@ class MigrationMangaController :
return MigrationMangaPresenter(sourceId)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = MigrationMangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = MigrationMangaAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter

View File

@ -5,10 +5,13 @@ import android.os.Bundle
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
@ -39,16 +42,16 @@ class SearchController(
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
}
fun migrateManga() {
val manga = manga ?: return
val newManga = newManga ?: return
fun migrateManga(manga: Manga? = null, newManga: Manga?) {
manga ?: return
newManga ?: return
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
}
fun copyManga() {
val manga = manga ?: return
val newManga = newManga ?: return
fun copyManga(manga: Manga? = null, newManga: Manga?) {
manga ?: return
newManga ?: return
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
}
@ -56,7 +59,7 @@ class SearchController(
override fun onMangaClick(manga: Manga) {
newManga = manga
val dialog =
MigrationDialog()
MigrationDialog(this.manga, newManga, this)
dialog.targetController = this
dialog.showDialog(router)
}
@ -75,7 +78,7 @@ class SearchController(
}
}
class MigrationDialog : DialogController() {
class MigrationDialog(private val manga: Manga? = null, private val newManga: Manga? = null, private val callingController: Controller? = null) : DialogController() {
private val preferences: PreferencesHelper by injectLazy()
@ -88,7 +91,7 @@ class SearchController(
)
return MaterialDialog(activity!!)
.message(R.string.migration_dialog_what_to_include)
.title(R.string.migration_dialog_what_to_include)
.listItemsMultiChoice(
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
initialSelection = preselected.toIntArray()
@ -101,12 +104,28 @@ class SearchController(
preferences.migrateFlags().set(newValue)
}
.positiveButton(R.string.migrate) {
(targetController as? SearchController)?.migrateManga()
if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.migrateManga(manga, newManga)
}
.negativeButton(R.string.copy) {
(targetController as? SearchController)?.copyManga()
if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.copyManga(manga, newManga)
}
.neutralButton(android.R.string.cancel)
}
}
override fun onTitleClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
router.pushController(SourceSearchController(manga, source, presenter.query).withFadeTransaction())
}
}

View File

@ -17,6 +17,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import java.util.Date
class SearchPresenter(
@ -56,11 +58,15 @@ class SearchPresenter(
replacingMangaRelay.call(true)
presenterScope.launchIO {
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
try {
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
migrateMangaInternal(source, chapters, prevManga, manga, replace)
} catch (e: Throwable) {
withUIContext { view?.applicationContext?.toast(e.message) }
}
migrateMangaInternal(source, chapters, prevManga, manga, replace)
}.invokeOnCompletion {
presenterScope.launchUI { replacingMangaRelay.call(false) }
}
}
@ -98,24 +104,22 @@ class SearchPresenter(
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
val maxChapterRead = prevMangaChapters
.filter { it.read }
.maxByOrNull { it.chapter_number }?.chapter_number
val bookmarkedChapters = prevMangaChapters
.filter { it.bookmark && it.isRecognizedNumber }
.map { it.chapter_number }
if (maxChapterRead != null) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber) {
if (chapter.chapter_number <= maxChapterRead) {
chapter.read = true
}
if (chapter.chapter_number in bookmarkedChapters) {
chapter.bookmark = true
}
.maxOfOrNull { it.chapter_number } ?: 0f
val dbChapters = db.getChapters(manga).executeAsBlocking()
for (chapter in dbChapters) {
if (chapter.isRecognizedNumber) {
val prevChapter = prevMangaChapters
.find { it.isRecognizedNumber && it.chapter_number == chapter.chapter_number }
if (prevChapter != null) {
chapter.date_fetch = prevChapter.date_fetch
chapter.bookmark = prevChapter.bookmark
}
if (chapter.chapter_number <= maxChapterRead) {
chapter.read = true
}
}
db.insertChapters(dbChapters).executeAsBlocking()
}
db.insertChapters(dbChapters).executeAsBlocking()
}
// Update categories

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import android.view.View
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
class SourceSearchController(
bundle: Bundle
) : BrowseSourceController(bundle) {
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
putSerializable(MANGA_KEY, manga)
if (searchQuery != null) {
putString(SEARCH_QUERY_KEY, searchQuery)
}
}
)
private var oldManga: Manga? = args.getSerializable(MANGA_KEY) as Manga?
private var newManga: Manga? = null
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
newManga = item.manga
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
val dialog =
SearchController.MigrationDialog(oldManga, newManga, this)
dialog.targetController = searchController
dialog.showDialog(router)
return true
}
private companion object {
const val MANGA_KEY = "oldManga"
}
}

View File

@ -5,8 +5,8 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
@ -29,14 +29,17 @@ class MigrationSourcesController :
return MigrationSourcesPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = MigrationSourcesControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SourceAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter

View File

@ -27,9 +27,14 @@ class MigrationSourcesPresenter(
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.asSequence().map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
.sortedBy { it.name.toLowerCase() }
.map { SourceItem(it, header) }.toList()
return library
.groupBy { it.source }
.filterKeys { it != LocalSource.ID }
.map {
val source = sourceManager.getOrStub(it.key)
SourceItem(source, it.value.size, header)
}
.sortedBy { it.source.name.toLowerCase() }
.toList()
}
}

View File

@ -7,7 +7,7 @@ import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
/**
* Item that contains the selection header.
@ -18,7 +18,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**
@ -45,7 +45,7 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
class Holder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
private val binding = SectionHeaderItemBinding.bind(view)
init {
binding.title.text = view.context.getString(R.string.migration_selection_prompt)

View File

@ -15,8 +15,8 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
fun bind(item: SourceItem) {
val source = item.source
binding.title.text = source.name
binding.subtitle.isVisible = true
binding.title.text = "${source.name} (${item.mangaCount})"
binding.subtitle.isVisible = source.lang != ""
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
itemView.post {

View File

@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information.
* @param header The header for this item.
*/
data class SourceItem(val source: Source, val header: SelectionHeader) :
data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/**

View File

@ -3,13 +3,13 @@ package eu.kanade.tachiyomi.ui.browse.source
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardHeaderBinding
import eu.kanade.tachiyomi.databinding.SectionHeaderItemBinding
import eu.kanade.tachiyomi.util.system.LocaleHelper
class LangHolder(view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter) {
private val binding = SourceMainControllerCardHeaderBinding.bind(view)
private val binding = SectionHeaderItemBinding.bind(view)
fun bind(item: LangItem) {
binding.title.text = LocaleHelper.getSourceDisplayName(item.code, itemView.context)

View File

@ -18,7 +18,7 @@ data class LangItem(val code: String) : AbstractHeaderItem<LangHolder>() {
* Returns the layout resource of this item.
*/
override fun getLayoutRes(): Int {
return R.layout.source_main_controller_card_header
return R.layout.section_header_item
}
/**

View File

@ -8,13 +8,12 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -26,18 +25,13 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -48,7 +42,7 @@ import uy.kohesive.injekt.api.get
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/
class SourceController :
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnSourceClickListener {
@ -72,21 +66,17 @@ class SourceController :
return SourcePresenter()
}
/**
* Initiate the view with [R.layout.source_main_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = SourceMainControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = SourceAdapter(this)
// Create recycler and set adapter.
@ -200,37 +190,6 @@ class SourceController :
parentController!!.router.pushController(controller.withFadeTransaction())
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.source_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(viewScope)
}
private fun performGlobalSearch(query: String) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
/**
* Called when an option menu item has been selected by the user.
*
@ -290,4 +249,21 @@ class SourceController :
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(
menu,
inflater,
R.menu.source_main,
R.id.action_search,
R.string.action_global_search_hint,
false // GlobalSearch handles the searching here
)
}
override fun onSearchViewQueryTextSubmit(query: String?) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
}

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.setVectorCompat
class SourceHolder(private val view: View, val adapter: SourceAdapter) :
@ -46,9 +45,9 @@ class SourceHolder(private val view: View, val adapter: SourceAdapter) :
binding.pin.isVisible = true
if (item.isPinned) {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, view.context.getResourceColor(R.attr.colorAccent))
binding.pin.setVectorCompat(R.drawable.ic_push_pin_24dp, R.attr.colorAccent)
} else {
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, view.context.getResourceColor(android.R.attr.textColorHint))
binding.pin.setVectorCompat(R.drawable.ic_push_pin_outline_24dp, android.R.attr.textColorHint)
}
}
}

View File

@ -49,12 +49,17 @@ data class SourceItem(
override fun equals(other: Any?): Boolean {
if (other is SourceItem) {
return source.id == other.source.id && getHeader()?.code == other.getHeader()?.code
return source.id == other.source.id &&
getHeader()?.code == other.getHeader()?.code &&
isPinned == other.isPinned
}
return false
}
override fun hashCode(): Int {
return source.id.hashCode() + (getHeader()?.code?.hashCode() ?: 0).toInt()
var result = source.id.hashCode()
result = 31 * result + (header?.hashCode() ?: 0)
result = 31 * result + isPinned.hashCode()
return result
}
}

View File

@ -8,7 +8,6 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
@ -19,6 +18,7 @@ import com.afollestad.materialdialogs.list.listItems
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -33,7 +33,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
@ -51,12 +51,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -64,7 +60,7 @@ import uy.kohesive.injekt.injectLazy
* Controller to manage the catalogues available in the app.
*/
open class BrowseSourceController(bundle: Bundle) :
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
@ -86,7 +82,7 @@ open class BrowseSourceController(bundle: Bundle) :
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
protected var adapter: FlexibleAdapter<IFlexible<*>>? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
@ -128,10 +124,7 @@ open class BrowseSourceController(bundle: Bundle) :
return BrowseSourcePresenter(args.getLong(SOURCE_ID_KEY), args.getString(SEARCH_QUERY_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = SourceControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
@ -247,6 +240,11 @@ open class BrowseSourceController(bundle: Bundle) :
actionFab?.shrinkOnScroll(recycler)
}
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
@ -259,25 +257,8 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.source_browse, menu)
// Initialize search menu
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { searchWithQuery(it.queryText.toString()) }
.launchIn(viewScope)
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
@ -285,6 +266,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (router.backstackSize >= 2 && router.backstack[router.backstackSize - 2].controller() is GlobalSearchController) {
router.popController(this)
} else {
nonSubmittedQuery = ""
searchWithQuery("")
}
@ -300,6 +282,10 @@ open class BrowseSourceController(bundle: Bundle) :
menu.findItem(displayItem).isChecked = true
}
override fun onSearchViewQueryTextSubmit(query: String?) {
searchWithQuery(query ?: "")
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)

View File

@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
*/
lateinit var source: CatalogueSource
/**
* Query from the view.
*/
var query = searchQuery ?: ""
private set
/**
* Modifiable list of filters.
*/
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
*/
private var pageSubscription: Subscription? = null
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
@ -17,10 +18,10 @@ class SourceFilterSheet(
onResetClicked: () -> Unit
) : BaseBottomSheetDialog(activity) {
private var filterNavView: FilterNavigationView
private var filterNavView: FilterNavigationView = FilterNavigationView(activity)
private val sheetBehavior: BottomSheetBehavior<*>
init {
filterNavView = FilterNavigationView(activity)
filterNavView.onFilterClicked = {
onFilterClicked()
this.dismiss()
@ -28,13 +29,23 @@ class SourceFilterSheet(
filterNavView.onResetClicked = onResetClicked
setContentView(filterNavView)
sheetBehavior = BottomSheetBehavior.from(filterNavView.parent as ViewGroup)
}
override fun show() {
super.show()
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
fun setFilters(items: List<IFlexible<*>>) {
filterNavView.adapter.updateDataSet(items)
}
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
class FilterNavigationView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) :
SimpleNavigationView(context, attrs) {
var onFilterClicked = {}
@ -42,9 +53,12 @@ class SourceFilterSheet(
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true)
private val binding = SourceFilterSheetBinding.inflate(LayoutInflater.from(context), null, false)
private val binding = SourceFilterSheetBinding.inflate(
LayoutInflater.from(context),
null,
false
)
init {
recycler.adapter = adapter

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -10,7 +11,6 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
@ -25,11 +25,9 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.wrapper.hint = filter.name
holder.edit.setText(filter.state)
holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
filter.state = text.toString()
}
})
holder.edit.doOnTextChanged { text, _, _, _ ->
filter.state = text.toString()
}
}
override fun equals(other: Any?): Boolean {

View File

@ -6,24 +6,19 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy
/**
@ -34,7 +29,7 @@ import uy.kohesive.injekt.injectLazy
open class GlobalSearchController(
protected val initialQuery: String? = null,
protected val extensionFilter: String? = null
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
GlobalSearchCardAdapter.OnMangaClickListener,
GlobalSearchAdapter.OnTitleClickListener {
@ -45,21 +40,16 @@ open class GlobalSearchController(
*/
protected var adapter: GlobalSearchAdapter? = null
/**
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
*/
private var optionsMenuSearchItem: MenuItem? = null
init {
setHasOptionsMenu(true)
}
/**
* Initiate the view with [R.layout.global_search_controller].
*
* @param inflater used to load the layout xml.
* @param container containing parent views.
* @return inflated view
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = GlobalSearchControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = GlobalSearchControllerBinding.inflate(inflater)
override fun getTitle(): String? {
return presenter.query
@ -100,36 +90,32 @@ open class GlobalSearchController(
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.global_search, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
createOptionsMenu(
menu,
inflater,
R.menu.global_search,
R.id.action_search,
null,
false // the onMenuItemActionExpand will handle this
)
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach {
presenter.search(it.queryText.toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
}
.launchIn(viewScope)
optionsMenuSearchItem = menu.findItem(R.id.action_search)
}
override fun onSearchMenuItemActionExpand(item: MenuItem?) {
super.onSearchMenuItemActionExpand(item)
val searchView = optionsMenuSearchItem?.actionView as SearchView
searchView.onActionViewExpanded() // Required to show the query in the view
if (nonSubmittedQuery.isBlank()) {
searchView.setQuery(presenter.query, false)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
presenter.search(query ?: "")
optionsMenuSearchItem?.collapseActionView()
setTitle() // Update toolbar title
}
/**
@ -140,6 +126,12 @@ open class GlobalSearchController(
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = GlobalSearchAdapter(this)
// Create recycler and set adapter.

View File

@ -47,12 +47,6 @@ open class GlobalSearchPresenter(
*/
val sources by lazy { getSourcesToQuery() }
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/

View File

@ -4,13 +4,13 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
@ -67,16 +67,7 @@ class CategoryController :
return resources?.getString(R.string.action_edit_categories)
}
/**
* Returns the view of this controller.
*
* @param inflater The layout inflater to create the view from XML.
* @param container The parent view for this one.
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = CategoriesControllerBinding.inflate(inflater)
/**
* Called after view inflation. Used to initialize the view.
@ -86,6 +77,12 @@ class CategoryController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = CategoryAdapter(this@CategoryController)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.setHasFixedSize(true)

View File

@ -5,11 +5,11 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
@ -54,10 +54,7 @@ class DownloadController :
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater)
override fun createPresenter(): DownloadPresenter {
return DownloadPresenter()
@ -70,6 +67,12 @@ class DownloadController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Check if download queue is empty and update information accordingly.
setInformationView()

View File

@ -14,9 +14,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
private var mangas = emptyList<Manga>()
private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>()
constructor(

View File

@ -6,6 +6,7 @@ import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
@ -82,6 +83,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
}
}
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = LibraryCategoryAdapter(this)
recycler.setHasFixedSize(true)

View File

@ -7,10 +7,8 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -19,6 +17,7 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
@ -27,8 +26,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
@ -37,11 +36,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.appcompat.queryTextChanges
import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Subscription
import uy.kohesive.injekt.Injekt
@ -50,7 +47,7 @@ import uy.kohesive.injekt.api.get
class LibraryController(
bundle: Bundle? = null,
private val preferences: PreferencesHelper = Injekt.get()
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
RootController,
TabbedController,
ActionMode.Callback,
@ -67,11 +64,6 @@ class LibraryController(
*/
private var actionMode: ActionMode? = null
/**
* Library search query.
*/
private var query: String = ""
/**
* Currently selected mangas.
*/
@ -171,14 +163,17 @@ class LibraryController(
return LibraryPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = LibraryControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = LibraryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
adapter = LibraryAdapter(this)
binding.libraryPager.adapter = adapter
binding.libraryPager.pageSelections()
@ -212,12 +207,12 @@ class LibraryController(
binding.btnGlobalSearch.clicks()
.onEach {
router.pushController(
GlobalSearchController(query).withFadeTransaction()
GlobalSearchController(presenter.query).withFadeTransaction()
)
}
.launchIn(viewScope)
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -230,7 +225,7 @@ class LibraryController(
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
binding.actionToolbar.destroy()
adapter?.onDestroy()
adapter = null
@ -384,52 +379,21 @@ class LibraryController(
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
performSearch()
// Workaround for weird behavior where searchview gets empty text change despite
// query being set already
searchView.postDelayed({ initSearchHandler(searchView) }, 500)
} else {
initSearchHandler(searchView)
}
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
// Mutate the filter icon because it needs to be tinted and the resource is shared.
menu.findItem(R.id.action_filter).icon.mutate()
}
fun search(query: String) {
this.query = query
}
private fun initSearchHandler(searchView: SearchView) {
searchView.queryTextChanges()
// Ignore events if this controller isn't at the top to avoid query being reset
.filter { router.backstack.lastOrNull()?.controller() == this }
.onEach {
query = it.toString()
performSearch()
}
.launchIn(viewScope)
presenter.query = query
}
private fun performSearch() {
searchRelay.call(query)
if (query.isNotEmpty()) {
searchRelay.call(presenter.query)
if (presenter.query.isNotEmpty()) {
binding.btnGlobalSearch.isVisible = true
binding.btnGlobalSearch.text =
resources?.getString(R.string.action_global_search_query, query)
resources?.getString(R.string.action_global_search_query, presenter.query)
} else {
binding.btnGlobalSearch.isVisible = false
}
@ -611,4 +575,12 @@ class LibraryController(
selectInverseRelay.call(it)
}
}
override fun onSearchViewQueryTextChange(newText: String?) {
// Ignore events if this controller isn't at the top to avoid query being reset
if (router.backstack.lastOrNull()?.controller() == this) {
presenter.query = newText ?: ""
performSearch()
}
}
}

View File

@ -90,6 +90,7 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) ||
(manga.description?.contains(constraint, true) ?: false) ||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
if (constraint.contains(",")) {
constraint.split(",").all { containsGenre(it.trim(), manga.getGenres()) }

View File

@ -235,7 +235,12 @@ class LibraryPresenter(
var counter = 0
db.getLatestChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
}
val chapterFetchDateManga by lazy {
var counter = 0
db.getChapterFetchDateManga().executeAsBlocking().associate { it.id!! to counter++ }
}
val sortAscending = preferences.librarySortingAscending().get()
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
when (sortingMode) {
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
@ -246,7 +251,13 @@ class LibraryPresenter(
manga1LastRead.compareTo(manga2LastRead)
}
LibrarySort.LAST_CHECKED -> i2.manga.last_update.compareTo(i1.manga.last_update)
LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
LibrarySort.UNREAD -> when {
// Ensure unread content comes first
i1.manga.unread == i2.manga.unread -> 0
i1.manga.unread == 0 -> if (sortAscending) 1 else -1
i2.manga.unread == 0 -> if (sortAscending) -1 else 1
else -> i1.manga.unread.compareTo(i2.manga.unread)
}
LibrarySort.TOTAL -> {
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
@ -259,12 +270,19 @@ class LibraryPresenter(
?: latestChapterManga.size
manga1latestChapter.compareTo(manga2latestChapter)
}
LibrarySort.CHAPTER_FETCH_DATE -> {
val manga1chapterFetchDate = chapterFetchDateManga[i1.manga.id!!]
?: chapterFetchDateManga.size
val manga2chapterFetchDate = chapterFetchDateManga[i2.manga.id!!]
?: chapterFetchDateManga.size
manga1chapterFetchDate.compareTo(manga2chapterFetchDate)
}
LibrarySort.DATE_ADDED -> i2.manga.date_added.compareTo(i1.manga.date_added)
else -> throw Exception("Unknown sorting mode")
}
}
val comparator = if (preferences.librarySortingAscending().get()) {
val comparator = if (sortAscending) {
Comparator(sortFn)
} else {
Collections.reverseOrder(sortFn)

View File

@ -20,7 +20,7 @@ class LibrarySettingsSheet(
router: Router,
private val trackManager: TrackManager = Injekt.get(),
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
) : TabbedBottomSheetDialog(router) {
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort
@ -157,11 +157,12 @@ class LibrarySettingsSheet(
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this)
private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
override val header = null
override val items =
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, dateAdded)
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded)
override val footer = null
override fun initModels() {
@ -184,6 +185,8 @@ class LibrarySettingsSheet(
if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
latestChapter.state =
if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE
chapterFetchDate.state =
if (sorting == LibrarySort.CHAPTER_FETCH_DATE) order else Item.MultiSort.SORT_NONE
dateAdded.state =
if (sorting == LibrarySort.DATE_ADDED) order else Item.MultiSort.SORT_NONE
}
@ -211,6 +214,7 @@ class LibrarySettingsSheet(
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
latestChapter -> LibrarySort.LATEST_CHAPTER
chapterFetchDate -> LibrarySort.CHAPTER_FETCH_DATE
dateAdded -> LibrarySort.DATE_ADDED
else -> throw Exception("Unknown sorting")
}

View File

@ -8,6 +8,7 @@ object LibrarySort {
const val UNREAD = 3
const val TOTAL = 4
const val LATEST_CHAPTER = 6
const val CHAPTER_FETCH_DATE = 8
const val DATE_ADDED = 7
@Deprecated("Removed in favor of searching by source")

View File

@ -2,12 +2,18 @@ package eu.kanade.tachiyomi.ui.main
import android.app.SearchManager
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceDialogController
@ -18,6 +24,7 @@ import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
@ -43,6 +50,9 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.InternalResourceHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import timber.log.Timber
@ -84,6 +94,39 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
// Draw edge-to-edge
WindowCompat.setDecorFitsSystemWindows(window, false)
binding.appbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
padding(left = true, top = true, right = true)
}
}
binding.rootFab.applyInsetter {
type(navigationBars = true) {
margin()
}
}
binding.bottomNav.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Make sure navigation bar is on bottom before we modify it
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
window.navigationBarColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
!InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true)
) {
Color.TRANSPARENT
} else {
// Set navbar scrim 70% of navigationBarColor
getResourceColor(android.R.attr.navigationBarColor, 0.7F)
}
}
insets
}
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
@ -110,6 +153,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
val controller = router.getControllerWithTag(id.toString()) as? LibraryController
controller?.showSettingsSheet()
}
R.id.nav_updates -> {
router.pushController(DownloadController().withFadeTransaction())
}
}
}
true
@ -301,11 +347,8 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
private suspend fun resetExitConfirmation() {
isConfirmingExit = true
val toast = Toast.makeText(this, R.string.confirm_exit, Toast.LENGTH_LONG)
toast.show()
val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
delay(2000)
toast.cancel()
isConfirmingExit = false
}
@ -406,7 +449,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
fun fixViewToBottom(view: View) {
val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
val maxAbsOffset = appBarLayout.measuredHeight - binding.tabs.measuredHeight
view.translationY = -maxAbsOffset - verticalOffset.toFloat()
view.translationY = -maxAbsOffset - verticalOffset.toFloat() + appBarLayout.marginTop
}
binding.appbar.addOnOffsetChangedListener(listener)
fixedViewsToBottom[view] = listener

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.ui.main
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle) {
@ -16,9 +15,7 @@ class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle
.title(text = activity!!.getString(R.string.updated_version, BuildConfig.VERSION_NAME))
.positiveButton(android.R.string.ok)
.neutralButton(R.string.whats_new) {
val url = "https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}")
}
}
}

View File

@ -11,7 +11,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.blue
@ -26,6 +25,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
@ -72,6 +72,7 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getCoordinates
@ -197,14 +198,22 @@ class MangaController :
)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MangaControllerBinding.inflate(inflater)
return binding.root
}
override fun createBinding(inflater: LayoutInflater) = MangaControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true)
}
}
if (manga == null || source == null) return
// Init RecyclerView and adapter
@ -242,7 +251,7 @@ class MangaController :
}
.launchIn(viewScope)
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
settingsSheet = ChaptersSettingsSheet(router, presenter) { group ->
if (group is ChaptersSettingsSheet.Filter.FilterGroup) {
@ -321,7 +330,7 @@ class MangaController :
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
binding.actionToolbar.destroy()
mangaInfoAdapter = null
chaptersHeaderAdapter = null
@ -608,8 +617,9 @@ class MangaController :
override fun openMangaCoverPicker(manga: Manga) {
if (manga.favorite) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
}
startActivityForResult(
Intent.createChooser(
intent,
@ -842,7 +852,6 @@ class MangaController :
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = chapters.all { it.chapter.read }
// Hide FAB to avoid interfering with the bottom action toolbar
// actionFab?.hide()
actionFab?.isVisible = false
}
return false
@ -874,10 +883,6 @@ class MangaController :
chaptersAdapter?.clearSelection()
selectedChapters.clear()
actionMode = null
// TODO: there seems to be a bug in MaterialComponents where the [ExtendedFloatingActionButton]
// fails to show up properly
// actionFab?.show()
actionFab?.isVisible = true
}
@ -988,7 +993,9 @@ class MangaController :
chapters.forEach {
chaptersAdapter?.updateItem(it)
}
chaptersAdapter?.notifyDataSetChanged()
launchUI {
chaptersAdapter?.notifyDataSetChanged()
}
}
fun onChaptersDeletedError(error: Throwable) {
@ -998,9 +1005,10 @@ class MangaController :
// OVERFLOW MENU DIALOGS
private fun getUnreadChaptersSorted() = presenter.chapters
.sortedWith(presenter.getChapterSort())
.filter { !it.read && it.status == Download.State.NOT_DOWNLOADED }
.distinctBy { it.name }
.sortedByDescending { it.source_order }
.reversed()
private fun downloadChapters(choice: Int) {
val chaptersToDownload = when (choice) {

View File

@ -429,7 +429,11 @@ class MangaPresenter(
observable = observable.filter { !it.bookmark }
}
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
return observable.toSortedList(getChapterSort())
}
fun getChapterSort(): (Chapter, Chapter) -> Int {
return when (manga.sorting) {
Manga.SORTING_SOURCE -> when (sortDescending()) {
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
@ -444,8 +448,6 @@ class MangaPresenter(
}
else -> throw NotImplementedError("Unimplemented sorting method")
}
return observable.toSortedList(sortFunction)
}
/**
@ -472,7 +474,7 @@ class MangaPresenter(
* Returns the next unread chapter or null if everything is read.
*/
fun getNextUnreadChapter(): ChapterItem? {
return chapters.sortedByDescending { it.source_order }.find { !it.read }
return chapters.sortedWith(getChapterSort()).findLast { !it.read }
}
/**

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.ui.manga.chapter
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
@ -59,8 +59,10 @@ class ChapterHolder(
descriptions.add(adapter.dateFormat.format(Date(chapter.date_upload)))
}
if (!chapter.read && chapter.last_page_read > 0) {
val lastPageRead = SpannableString(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)).apply {
setSpan(ForegroundColorSpan(adapter.readColor), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
val lastPageRead = buildSpannedString {
color(adapter.readColor) {
append(itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1))
}
}
descriptions.add(lastPageRead)
}

View File

@ -17,7 +17,7 @@ class ChaptersSettingsSheet(
private val router: Router,
private val presenter: MangaPresenter,
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
) : TabbedBottomSheetDialog(router) {
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort

View File

@ -55,24 +55,24 @@ class TrackSearchAdapter(context: Context) :
.into(binding.trackSearchCover)
}
if (track.publishing_status.isBlank()) {
binding.trackSearchStatus.isVisible = false
binding.trackSearchStatusResult.isVisible = false
} else {
val hasStatus = track.publishing_status.isNotBlank()
binding.trackSearchStatus.isVisible = hasStatus
binding.trackSearchStatusResult.isVisible = hasStatus
if (hasStatus) {
binding.trackSearchStatusResult.text = track.publishing_status.capitalize()
}
if (track.publishing_type.isBlank()) {
binding.trackSearchType.isVisible = false
binding.trackSearchTypeResult.isVisible = false
} else {
val hasType = track.publishing_type.isNotBlank()
binding.trackSearchType.isVisible = hasType
binding.trackSearchTypeResult.isVisible = hasType
if (hasType) {
binding.trackSearchTypeResult.text = track.publishing_type.capitalize()
}
if (track.start_date.isBlank()) {
binding.trackSearchStart.isVisible = false
binding.trackSearchStartResult.isVisible = false
} else {
val hasStartDate = track.start_date.isNotBlank()
binding.trackSearchStart.isVisible = hasStartDate
binding.trackSearchStartResult.isVisible = hasStartDate
if (hasStartDate) {
binding.trackSearchStartResult.text = track.start_date
}
}

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
@ -65,7 +64,7 @@ class TrackSheet(
val track = adapter.getItem(position)?.track ?: return
if (track.tracking_url.isNotBlank()) {
controller.activity?.startActivity(Intent(Intent.ACTION_VIEW, track.tracking_url.toUri()))
controller.openInBrowser(track.tracking_url)
}
}

View File

@ -1,10 +1,7 @@
package eu.kanade.tachiyomi.ui.more
import android.app.Dialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog
@ -15,7 +12,9 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
import eu.kanade.tachiyomi.util.preference.onClick
@ -76,19 +75,15 @@ class AboutController : SettingsController() {
} else {
"https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
}
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
openInBrowser(url)
}
}
if (BuildConfig.DEBUG) {
preference {
key = "pref_about_notices"
titleRes = R.string.notices
onClick {
val intent = Intent(Intent.ACTION_VIEW, "https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md".toUri())
startActivity(intent)
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md")
}
}
}
@ -97,53 +92,53 @@ class AboutController : SettingsController() {
preference {
key = "pref_about_website"
titleRes = R.string.website
val url = "https://tachiyomi.org"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://tachiyomi.org".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_twitter"
title = "Twitter"
"https://twitter.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_discord"
title = "Discord"
val url = "https://discord.gg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://discord.gg/tachiyomi".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_github"
title = "GitHub"
val url = "https://github.com/tachiyomiorg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://github.com/tachiyomiorg/tachiyomi".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_label_extensions"
titleRes = R.string.label_extensions
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://github.com/tachiyomiorg/tachiyomi-extensions".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_licenses"
titleRes = R.string.licenses
onClick {
LibsBuilder()
.withActivityTitle(activity!!.getString(R.string.licenses))
.withAboutIconShown(false)
.withAboutVersionShown(false)
.withLicenseShown(true)
.withEdgeToEdge(true)
.start(activity!!)
}
}
@ -207,19 +202,10 @@ class AboutController : SettingsController() {
}
private fun copyDebugInfo() {
val deviceInfo =
"""
App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}, ${BuildConfig.COMMIT_SHA}, ${BuildConfig.VERSION_CODE})
Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})
Android build ID: ${Build.DISPLAY}
Device brand: ${Build.BRAND}
Device manufacturer: ${Build.MANUFACTURER}
Device name: ${Build.DEVICE}
Device model: ${Build.MODEL}
Device product name: ${Build.PRODUCT}
""".trimIndent()
activity?.copyToClipboard("Debug information", deviceInfo)
activity?.let {
val deviceInfo = CrashLogUtil(it).getDebugInfo()
activity?.copyToClipboard("Debug information", deviceInfo)
}
}
private fun getFormattedBuildTime(): String {

View File

@ -1,7 +1,11 @@
package eu.kanade.tachiyomi.ui.more
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
@ -26,7 +30,10 @@ import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
@ -39,6 +46,9 @@ class MoreController :
private var isDownloading: Boolean = false
private var downloadQueueSize: Int = 0
private var untilDestroySubscriptions = CompositeSubscription()
private set
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_more
@ -78,7 +88,7 @@ class MoreController :
}
}
preference {
titleRes = R.string.label_categories
titleRes = R.string.categories
iconRes = R.drawable.ic_label_24dp
iconTint = tintColor
onClick {
@ -115,6 +125,19 @@ class MoreController :
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
untilDestroySubscriptions.unsubscribe()
}
private fun initDownloadQueueSummary(preference: Preference) {
// Handle running/paused status change
DownloadService.runningRelay
@ -141,6 +164,10 @@ class MoreController :
}
}
private fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
}
private class MoreHeaderPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
Preference(context, attrs) {

View File

@ -6,8 +6,6 @@ import android.app.ProgressDialog
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
@ -16,18 +14,17 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.SeekBar
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -35,14 +32,20 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.data.preference.toggle
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
@ -55,8 +58,8 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar
import eu.kanade.tachiyomi.util.view.isDefaultBar
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.util.view.showBar
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.coroutines.delay
@ -77,6 +80,16 @@ import kotlin.math.abs
@RequiresPresenter(ReaderPresenter::class)
class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() {
companion object {
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
private val preferences: PreferencesHelper by injectLazy()
/**
@ -109,22 +122,9 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
@Suppress("DEPRECATION")
private var progressDialog: ProgressDialog? = null
companion object {
@Suppress("unused")
const val LEFT_TO_RIGHT = 1
const val RIGHT_TO_LEFT = 2
const val VERTICAL = 3
const val WEBTOON = 4
const val VERTICAL_PLUS = 5
private var menuToggleToast: Toast? = null
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
private var readingModeToast: Toast? = null
/**
* Called when the activity is created. Initializes the presenter and configuration.
@ -174,6 +174,8 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
viewer?.destroy()
viewer = null
config = null
menuToggleToast?.cancel()
readingModeToast?.cancel()
progressDialog?.dismiss()
progressDialog = null
}
@ -237,18 +239,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
presenter.bookmarkCurrentChapter(false)
invalidateOptionsMenu()
}
R.id.action_settings -> ReaderSettingsSheet(this).show()
R.id.action_custom_filter -> {
val sheet = ReaderColorFilterSheet(this)
// Remove dimmed backdrop so changes can be previewed
.apply { window?.setDimAmount(0f) }
// Hide toolbars while sheet is open for better preview
sheet.setOnDismissListener { setMenuVisibility(true) }
setMenuVisibility(false)
sheet.show()
}
}
return super.onOptionsItemSelected(item)
}
@ -294,7 +284,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
// Set toolbar
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
@ -314,6 +303,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
insets
}
binding.toolbar.setOnClickListener {
presenter.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA
putExtra(MangaController.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
)
}
}
// Init listeners on bottom menu
binding.pageSeekbar.setOnSeekBarChangeListener(
object : SimpleSeekBarListener() {
@ -343,15 +344,110 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
}
}
initBottomShortcuts()
// Set initial visibility
setMenuVisibility(menuVisible)
}
private fun initBottomShortcuts() {
// Reading mode
with(binding.actionReadingMode) {
setTooltip(R.string.viewer)
setOnClickListener {
val newReadingMode =
ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false))
presenter.setMangaViewer(newReadingMode.prefValue)
menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
}
}
// Rotation
with(binding.actionRotation) {
setTooltip(R.string.pref_rotation_type)
setOnClickListener {
val newOrientation =
OrientationType.getNextOrientation(preferences.rotation().get(), resources)
preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
}
}
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
.launchIn(lifecycleScope)
// Crop borders
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
setOnClickListener {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
if (isPagerType) {
preferences.cropBorders().toggle()
} else {
preferences.cropBordersWebtoon().toggle()
}
}
}
updateCropBordersShortcut()
listOf(preferences.cropBorders(), preferences.cropBordersWebtoon())
.forEach { pref ->
pref.asFlow()
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
// Settings sheet
with(binding.actionSettings) {
setTooltip(R.string.action_settings)
setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show()
}
setOnLongClickListener {
ReaderSettingsSheet(this@ReaderActivity, showColorFilterSettings = true).show()
true
}
}
}
private fun updateRotationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference, resources)
binding.actionRotation.setImageResource(orientation.iconRes)
}
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
val enabled = if (isPagerType) {
preferences.cropBorders().get()
} else {
preferences.cropBordersWebtoon().get()
}
binding.actionCropBorders.setImageResource(
if (enabled) {
R.drawable.ic_crop_24dp
} else {
R.drawable.ic_crop_off_24dp
}
)
}
/**
* Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views.
*/
private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
menuVisible = visible
if (visible) {
if (preferences.fullscreen().get()) {
@ -366,7 +462,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationStart(animation: Animation) {
// Fix status bar being translucent the first time it's opened.
// Fix status bar being translucent the first time it's opened.
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}
@ -422,12 +518,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
*/
fun setManga(manga: Manga) {
val prevViewer = viewer
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaViewer(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = when (presenter.getMangaViewer()) {
RIGHT_TO_LEFT -> R2LPagerViewer(this)
VERTICAL -> VerticalPagerViewer(this)
WEBTOON -> WebtoonViewer(this)
VERTICAL_PLUS -> WebtoonViewer(this, isContinuous = false)
else -> L2RPagerViewer(this)
ReadingModeType.LEFT_TO_RIGHT.prefValue -> L2RPagerViewer(this)
ReadingModeType.VERTICAL.prefValue -> VerticalPagerViewer(this)
ReadingModeType.WEBTOON.prefValue -> WebtoonViewer(this)
ReadingModeType.CONTINUOUS_VERTICAL.prefValue -> WebtoonViewer(this, isContinuous = false)
else -> R2LPagerViewer(this)
}
// Destroy previous viewer if there was one
@ -439,20 +539,32 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView())
if (preferences.showReadingMode()) {
showReadingModeSnackbar(presenter.getMangaViewer())
showReadingModeToast(presenter.getMangaViewer())
}
binding.toolbar.title = manga.title
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer
if (newViewer is R2LPagerViewer) {
binding.leftChapter.setTooltip(R.string.action_next_chapter)
binding.rightChapter.setTooltip(R.string.action_previous_chapter)
} else {
binding.leftChapter.setTooltip(R.string.action_previous_chapter)
binding.rightChapter.setTooltip(R.string.action_next_chapter)
}
binding.pleaseWait.isVisible = true
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
}
private fun showReadingModeSnackbar(mode: Int) {
val strings = resources.getStringArray(R.array.viewers_selector)
binding.root.snack(strings[mode], Snackbar.LENGTH_SHORT)
private fun showReadingModeToast(mode: Int) {
try {
val strings = resources.getStringArray(R.array.viewers_selector)
readingModeToast?.cancel()
readingModeToast = toast(strings[mode])
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.e("Unknown reading mode: $mode")
}
}
/**
@ -652,6 +764,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
)
}
/**
* Forces the user preferred [orientation] on the activity.
*/
private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation, resources)
if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag
}
}
/**
* Class that handles the user preferences of the reader.
*/
@ -705,38 +827,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
.launchIn(lifecycleScope)
}
/**
* Forces the user preferred [orientation] on the activity.
*/
private fun setOrientation(orientation: Int) {
val newOrientation = when (orientation) {
// Lock in current orientation
2 -> {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}
// Lock in portrait
3 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
// Lock in landscape
4 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
// Rotation free
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
if (newOrientation != requestedOrientation) {
requestedOrientation = newOrientation
}
}
/**
* Sets the visibility of the bottom page indicator according to [visible].
*/
fun setPageNumberVisibility(visible: Boolean) {
binding.pageNumber.visibility = if (visible) View.VISIBLE else View.INVISIBLE
binding.pageNumber.isVisible = visible
}
/**

View File

@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewPropertyAnimator
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import kotlin.math.abs
class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private var viewPropertyAnimator: ViewPropertyAnimator? = null
private var navigation: ViewerNavigation? = null
fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) {
if (!showOnStart && this.navigation == null) {
this.navigation = navigation
isVisible = false
return
}
this.navigation = navigation
invalidate()
if (isVisible) return
viewPropertyAnimator = animate()
.alpha(1f)
.setDuration(FADE_DURATION)
.withStartAction {
isVisible = true
}
.withEndAction {
viewPropertyAnimator = null
}
viewPropertyAnimator?.start()
}
private val regionPaint = Paint()
private val textPaint = Paint().apply {
textAlign = Paint.Align.CENTER
color = Color.WHITE
textSize = 64f
}
private val textBorderPaint = Paint().apply {
textAlign = Paint.Align.CENTER
color = Color.BLACK
textSize = 64f
style = Paint.Style.STROKE
strokeWidth = 8f
}
override fun onDraw(canvas: Canvas?) {
if (navigation == null) return
navigation?.regions?.forEach { region ->
val rect = region.rectF
canvas?.save()
// Scale rect from 1f,1f to screen width and height
canvas?.scale(width.toFloat(), height.toFloat())
regionPaint.color = ContextCompat.getColor(context, region.type.colorRes)
canvas?.drawRect(rect, regionPaint)
canvas?.restore()
// Don't want scale anymore because it messes with drawText
canvas?.save()
// Translate origin to rect start (left, top)
canvas?.translate((width * rect.left), (height * rect.top))
// Calculate center of rect width on screen
val x = width * (abs(rect.left - rect.right) / 2)
// Calculate center of rect height on screen
val y = height * (abs(rect.top - rect.bottom) / 2)
canvas?.drawText(context.getString(region.type.nameRes), x, y, textBorderPaint)
canvas?.drawText(context.getString(region.type.nameRes), x, y, textPaint)
canvas?.restore()
}
}
override fun performClick(): Boolean {
super.performClick()
if (viewPropertyAnimator == null && isVisible) {
viewPropertyAnimator = animate()
.alpha(0f)
.setDuration(FADE_DURATION)
.withEndAction {
isVisible = false
viewPropertyAnimator = null
}
viewPropertyAnimator?.start()
}
return true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
// Hide overlay if user start tapping or swiping
performClick()
return super.onTouchEvent(event)
}
}
private const val FADE_DURATION = 1000L

View File

@ -489,9 +489,9 @@ class ReaderPresenter(
/**
* Returns the viewer position used by this manga or the default one.
*/
fun getMangaViewer(): Int {
fun getMangaViewer(resolveDefault: Boolean = true): Int {
val manga = manga ?: return preferences.defaultViewer()
return if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
return if (resolveDefault && manga.viewer == 0) preferences.defaultViewer() else manga.viewer
}
/**
@ -559,7 +559,7 @@ class ReaderPresenter(
val destDir = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + "Tachiyomi"
File.separator + context.getString(R.string.app_name)
)
// Copy file in background.

View File

@ -1,157 +0,0 @@
package eu.kanade.tachiyomi.ui.reader
import android.os.Bundle
import android.widget.CompoundButton
import android.widget.Spinner
import androidx.annotation.ArrayRes
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderSettingsSheetBinding
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.injectLazy
/**
* Sheet to show reader and viewer preferences.
*/
class ReaderSettingsSheet(private val activity: ReaderActivity) : BaseBottomSheetDialog(activity) {
private val preferences: PreferencesHelper by injectLazy()
private val binding = ReaderSettingsSheetBinding.inflate(activity.layoutInflater, null, false)
init {
val scroll = NestedScrollView(activity)
scroll.addView(binding.root)
setContentView(scroll)
}
/**
* Called when the sheet is created. It initializes the listeners and values of the preferences.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initGeneralPreferences()
when (activity.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}
}
/**
* Init general reader preferences.
*/
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
activity.presenter.setMangaViewer(position)
val mangaViewer = activity.presenter.getMangaViewer()
if (mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS) {
initWebtoonPreferences()
} else {
initPagerPreferences()
}
}
binding.viewer.setSelection(activity.presenter.manga?.viewer ?: 0, false)
binding.rotationMode.bindToPreference(preferences.rotation(), 1)
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
binding.showPageNumber.bindToPreference(preferences.showPageNumber())
binding.fullscreen.bindToPreference(preferences.fullscreen())
binding.dualPageSplit.bindToPreference(preferences.dualPageSplit())
binding.keepscreen.bindToPreference(preferences.keepScreenOn())
binding.longTap.bindToPreference(preferences.readWithLongTap())
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())
binding.pageTransitions.bindToPreference(preferences.pageTransitions())
// If the preference is explicitly disabled, that means the setting was configured since there is a cutout
if (activity.hasCutout || !preferences.cutoutShort().get()) {
binding.cutoutShort.isVisible = true
binding.cutoutShort.bindToPreference(preferences.cutoutShort())
}
}
/**
* Init the preferences for the pager reader.
*/
private fun initPagerPreferences() {
binding.webtoonPrefsGroup.root.isVisible = false
binding.pagerPrefsGroup.root.isVisible = true
binding.pagerPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
binding.pagerPrefsGroup.tappingInverted.bindToPreference(preferences.pagerNavInverted())
binding.pagerPrefsGroup.pagerNav.bindToPreference(preferences.navigationModePager())
binding.pagerPrefsGroup.scaleType.bindToPreference(preferences.imageScaleType(), 1)
binding.pagerPrefsGroup.zoomStart.bindToPreference(preferences.zoomStart(), 1)
binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
}
/**
* Init the preferences for the webtoon reader.
*/
private fun initWebtoonPreferences() {
binding.pagerPrefsGroup.root.isVisible = false
binding.webtoonPrefsGroup.root.isVisible = true
binding.webtoonPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(preferences.webtoonNavInverted())
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
}
/**
* Binds a checkbox or switch view with a boolean preference.
*/
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
isChecked = pref.get()
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
}
/**
* Binds a spinner to an int preference with an optional offset for the value.
*/
private fun Spinner.bindToPreference(pref: Preference<Int>, offset: Int = 0) {
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
pref.set(position + offset)
}
setSelection(pref.get() - offset, false)
}
/**
* Binds a spinner to an enum preference.
*/
private inline fun <reified T : Enum<T>> Spinner.bindToPreference(pref: Preference<T>) {
val enumConstants = T::class.java.enumConstants
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
enumConstants?.get(position)?.let { pref.set(it) }
}
enumConstants?.indexOf(pref.get())?.let { setSelection(it, false) }
}
/**
* Binds a spinner to an int preference. The position of the spinner item must
* correlate with the [intValues] resource item (in arrays.xml), which is a <string-array>
* of int values that will be parsed here and applied to the preference.
*/
private fun Spinner.bindToIntPreference(pref: Preference<Int>, @ArrayRes intValuesResource: Int) {
val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() }
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
pref.set(intValues[position]!!)
}
setSelection(intValues.indexOf(pref.get()), false)
}
}

View File

@ -85,8 +85,7 @@ class HttpPageLoader(
* the local cache, otherwise fallbacks to network.
*/
override fun getPages(): Observable<List<ReaderPage>> {
return chapterCache
.getPageListFromCache(chapter.chapter)
return Observable.fromCallable { chapterCache.getPageListFromCache(chapter.chapter) }
.onErrorResumeNext { source.fetchPageList(chapter.chapter) }
.map { pages ->
pages.mapIndexed { index, page ->

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.lang.next
enum class OrientationType(val prefValue: Int, val flag: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
FREE(1, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED, R.string.rotation_free, R.drawable.ic_screen_rotation_24dp),
LOCKED_PORTRAIT(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
LOCKED_LANDSCAPE(2, ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE, R.string.rotation_lock, R.drawable.ic_screen_lock_rotation_24dp),
PORTRAIT(3, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT, R.string.rotation_force_portrait, R.drawable.ic_screen_lock_portrait_24dp),
LANDSCAPE(4, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE, R.string.rotation_force_landscape, R.drawable.ic_screen_lock_landscape_24dp);
companion object {
fun fromPreference(preference: Int, resources: Resources): OrientationType = when (preference) {
2 -> {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
LOCKED_PORTRAIT
} else {
LOCKED_LANDSCAPE
}
}
3 -> PORTRAIT
4 -> LANDSCAPE
else -> FREE
}
fun getNextOrientation(preference: Int, resources: Resources): OrientationType {
val current = if (preference == 2) {
// Avoid issue due to 2 types having the same prefValue
LOCKED_LANDSCAPE
} else {
fromPreference(preference, resources)
}
return current.next()
}
}
}

View File

@ -1,19 +1,21 @@
package eu.kanade.tachiyomi.ui.reader
package eu.kanade.tachiyomi.ui.reader.setting
import android.view.ViewGroup
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.SeekBar
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderColorFilterSheetBinding
import eu.kanade.tachiyomi.databinding.ReaderColorFilterSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
@ -22,30 +24,27 @@ import uy.kohesive.injekt.injectLazy
/**
* Color filter sheet to toggle custom filter and brightness overlay.
*/
class ReaderColorFilterSheet(private val activity: ReaderActivity) : BaseBottomSheetDialog(activity) {
class ReaderColorFilterSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestedScrollView(context, attrs) {
private val preferences: PreferencesHelper by injectLazy()
private var sheetBehavior: BottomSheetBehavior<*>? = null
private val binding = ReaderColorFilterSheetBinding.inflate(activity.layoutInflater, null, false)
private val binding = ReaderColorFilterSettingsBinding.inflate(LayoutInflater.from(context), this, false)
init {
setContentView(binding.root)
sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
addView(binding.root)
preferences.colorFilter().asFlow()
.onEach { setColorFilter(it) }
.launchIn(activity.lifecycleScope)
.launchIn((context as ReaderActivity).lifecycleScope)
preferences.colorFilterMode().asFlow()
.onEach { setColorFilter(preferences.colorFilter().get()) }
.launchIn(activity.lifecycleScope)
.launchIn(context.lifecycleScope)
preferences.customBrightness().asFlow()
.onEach { setCustomBrightness(it) }
.launchIn(activity.lifecycleScope)
.launchIn(context.lifecycleScope)
// Get color and update values
val color = preferences.colorFilterValue().get()
@ -130,12 +129,6 @@ class ReaderColorFilterSheet(private val activity: ReaderActivity) : BaseBottomS
)
}
override fun onStart() {
super.onStart()
sheetBehavior?.skipCollapsed = true
sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
}
/**
* Set enabled status of seekBars belonging to color filter
* @param enabled determines if seekBar gets enabled
@ -183,7 +176,7 @@ class ReaderColorFilterSheet(private val activity: ReaderActivity) : BaseBottomS
preferences.customBrightnessValue().asFlow()
.sample(100)
.onEach { setCustomBrightnessValue(it) }
.launchIn(activity.lifecycleScope)
.launchIn((context as ReaderActivity).lifecycleScope)
} else {
setCustomBrightnessValue(0, true)
}
@ -211,7 +204,7 @@ class ReaderColorFilterSheet(private val activity: ReaderActivity) : BaseBottomS
preferences.colorFilterValue().asFlow()
.sample(100)
.onEach { setColorFilterValue(it) }
.launchIn(activity.lifecycleScope)
.launchIn((context as ReaderActivity).lifecycleScope)
}
setColorFilterSeekBar(enabled)
}

View File

@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderGeneralSettingsBinding
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.preference.bindToPreference
import uy.kohesive.injekt.injectLazy
/**
* Sheet to show reader and viewer preferences.
*/
class ReaderGeneralSettings @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestedScrollView(context, attrs) {
private val preferences: PreferencesHelper by injectLazy()
private val binding = ReaderGeneralSettingsBinding.inflate(LayoutInflater.from(context), this, false)
init {
addView(binding.root)
initGeneralPreferences()
}
/**
* Init general reader preferences.
*/
private fun initGeneralPreferences() {
binding.rotationMode.bindToPreference(preferences.rotation(), 1)
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
binding.showPageNumber.bindToPreference(preferences.showPageNumber())
binding.fullscreen.bindToPreference(preferences.fullscreen())
binding.keepscreen.bindToPreference(preferences.keepScreenOn())
binding.longTap.bindToPreference(preferences.readWithLongTap())
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())
binding.pageTransitions.bindToPreference(preferences.pageTransitions())
// If the preference is explicitly disabled, that means the setting was configured since there is a cutout
if ((context as ReaderActivity).hasCutout || !preferences.cutoutShort().get()) {
binding.cutoutShort.isVisible = true
binding.cutoutShort.bindToPreference(preferences.cutoutShort())
}
}
}

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